Whiskey.psm1
using module '.\Whiskey.Types.psm1' $startedAt = Get-Date function Write-Timing { param( [Parameter(Position=0)] $Message ) $now = Get-Date Write-Debug -Message ('[{0:hh":"mm":"ss"."ff}] {1}' -f ($now - $startedAt),$Message) } [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] $psModulesDirectoryName = 'PSModules' $whiskeyScriptRoot = $PSScriptRoot $whiskeyBinPath = Join-Path -Path $whiskeyScriptRoot -ChildPath 'bin' -Resolve [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] $buildStartedAt = [DateTime]::MinValue $PSModuleAutoLoadingPreference = 'None' # The indentation to use when writing task messages. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] $taskWriteIndent = ' ' [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] $indentLevel = 0 # PowerShell 5.1 doesn't have these variables so create them if they don't exist. if( -not (Get-Variable -Name 'IsLinux' -ErrorAction Ignore) ) { # We only set these variables on platforms where they're not defined. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidAssignmentToAutomaticVariable', '')] $IsLinux = $false [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidAssignmentToAutomaticVariable', '')] $IsMacOS = $false [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidAssignmentToAutomaticVariable', '')] $IsWindows = $true } if( -not (Test-Path -Path 'env:WHISKEY_DISABLE_ERROR_FORMAT') ) { Write-Timing 'Updating formats.' $prependFormats = @( (Join-Path -Path $PSScriptRoot -ChildPath 'Formats\System.Management.Automation.ErrorRecord.ps1xml'), (Join-Path -Path $PSScriptRoot -ChildPath 'Formats\System.Exception.ps1xml') ) Update-FormatData -PrependPath $prependFormats } Write-Timing 'Loading assemblies.' # .NET Framework 4.6.2 won't load netstandard2.0 assemblies. $frameworkMoniker = 'netstandard2.0' $net4Key = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -ErrorAction Ignore $net462Release = 460798 if( $net4Key -and $net4Key.Release -le $net462Release ) { $frameworkMoniker = 'net452' } foreach( $assemblyBaseName in @('SemanticVersion', 'YamlDotNet', 'Whiskey') ) { $assemblyPath = Join-Path -Path $whiskeyBinPath -ChildPath $frameworkMoniker $assemblyPath = Join-Path -Path $assemblyPath -ChildPath "$($assemblyBaseName).dll" $addTypeErrors = @() Add-Type -Path $assemblyPath -ErrorVariable 'addTypeErrors' -ErrorAction Continue if( $addTypeErrors ) { $ex = $addTypeErrors | Select-Object -First 1 while( $ex.InnerException ) { $ex = $ex.InnerException } if( $ex | Get-Member 'LoaderExceptions' ) { $ex = $ex.LoaderExceptions | Select-Object -First 1 } Write-Error -Message "Exception loading assembly ""$($assemblyPath)"": $($ex)" -ErrorAction Stop } } Add-Type -AssemblyName 'System.IO.Compression.FileSystem' Write-Timing 'Updating serialiazation depths on Whiskey objects.' # Make sure our custom objects get serialized/deserialized correctly, otherwise they don't get passed to PowerShell tasks correctly. Update-TypeData -TypeName 'Whiskey.BuildContext' -SerializationDepth 50 -ErrorAction Ignore Update-TypeData -TypeName 'Whiskey.BuildInfo' -SerializationDepth 50 -ErrorAction Ignore Update-TypeData -TypeName 'Whiskey.BuildVersion' -SerializationDepth 50 -ErrorAction Ignore Write-Timing 'Testing that correct Whiskey assembly is loaded.' $oldVersionLoadedMsg = 'You''ve got an old version of Whiskey loaded. Please open a new PowerShell session.' function New-WhiskeyObject { param( [Parameter(Mandatory)] [String]$TypeName, [Object[]]$ArgumentList ) try { return (New-Object -TypeName $TypeName -ArgumentList $ArgumentList -ErrorAction Ignore) } catch { Write-Error -Message ('Unable to find type "{0}". {1}' -f $TypeName,$oldVersionLoadedMsg) -ErrorAction Stop } } function Assert-Member { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowNull()] [Object]$Object, [String[]]$Property = @() ) $oldVersionLoadedMsg = 'You''ve got an old version of Whiskey loaded. Please open a new PowerShell session.' if( -not $Object ) { Write-Error -Message $oldVersionLoadedMsg -ErrorAction Stop } foreach( $propertyToCheck in $Property ) { if( -not ($Object | Get-Member $propertyToCheck) ) { $msg = 'Object "{0}" is missing member "{1}".' -f $Object.GetType().FullName,$propertyToCheck Write-Error -Message ('{0} {1}' -f $msg,$oldVersionLoadedMsg) -ErrorAction Stop } } } Write-Timing 'Checking Whiskey assembly loaded.' $context = New-WhiskeyObject -TypeName 'Whiskey.Context' Assert-Member -Object $context ` -Property @( 'TaskPaths', 'MSBuildConfiguration', 'ApiKeys', 'BuildStopwatch', 'TaskStopwatch' ) Write-Timing 'Checking Whiskey.TaskAttribute class.' $attr = New-WhiskeyObject -TypeName 'Whiskey.TaskAttribute' -ArgumentList 'Whiskey' Assert-Member -Object $attr -Property @( 'Aliases', 'WarnWhenUsingAlias', 'Obsolete', 'ObsoleteMessage', 'Platform' ) Write-Timing 'Checking for Whiskey.RequiresPowerShellModuleAttribute class.' New-WhiskeyObject -TypeName 'Whiskey.RequiresPowerShellModuleAttribute' -ArgumentList ('Whiskey') | Out-Null $attr = New-WhiskeyObject -TypeName 'Whiskey.Tasks.ValidatePathAttribute' Assert-Member -Object $attr -Property @( 'Create' ) $buildInfo = New-WhiskeyObject -TypeName 'Whiskey.BuildInfo' Assert-Member -Object $buildInfo -Property @( 'ScmSourceBranch' ) New-WhiskeyObject -TypeName 'Whiskey.RequiresNuGetPackageAttribute' -ArgumentList 'NuGet.CommandLine' | Out-Null [Type]$apiKeysType = $context.ApiKeys.GetType() $apiKeysDictGenericTypes = $apiKeysType.GenericTypeArguments if( -not $apiKeysDictGenericTypes -or $apiKeysDictGenericTypes.Count -ne 2 -or $apiKeysDictGenericTypes[1].FullName -ne [securestring].FullName ) { Write-Error -Message $oldVersionLoadedMsg -ErrorAction Stop } Write-Timing ('Creating internal module variables.') $dotNetExeName = 'dotnet' $nodeExeName = 'node' [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] $nodeDirName = 'bin' if( $IsWindows ) { $dotNetExeName = '{0}.exe' -f $dotNetExeName $nodeExeName = '{0}.exe' -f $nodeExeName $nodeDirName = '' } [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] $CurrentPlatform = [Whiskey.Platform]::Unknown if( $IsLinux ) { $CurrentPlatform = [Whiskey.Platform]::Linux } elseif( $IsMacOS ) { $CurrentPlatform = [Whiskey.Platform]::MacOS } elseif( $IsWindows ) { $CurrentPlatform = [Whiskey.Platform]::Windows } Write-Timing -Message ('Dot-sourcing files.') $count = 0 & { Join-Path -Path $PSScriptRoot -ChildPath 'Functions' Join-Path -Path $PSScriptRoot -ChildPath 'Tasks' } | Where-Object { Test-Path -Path $_ } | Get-ChildItem -Filter '*.ps1' | ForEach-Object { $count += 1 . $_.FullName } Write-Timing -Message ('Finished dot-sourcing {0} files.' -f $count) function Add-WhiskeyApiKey { <# .SYNOPSIS Adds an API key to Whiskey's API key store. .DESCRIPTION The `Add-WhiskeyApiKey` function adds an API key to Whiskey's API key store. Tasks that need API keys usually have a property where you provide the ID of the API key to use. You provide Whiskey the value of the API Key with this function. For example, if you are publishing a PowerShell module, your `whiskey.yml` file will look something like this: Publish: - PublishPowerShellModule: RepositoryName: PSGallery Path: Whiskey ApiKeyID: PSGalleryApiKey After you create your build's context with `New-WhiskeyContext`, you would then call `Add-WhiskeyApiKey` to add the actual API key: $context = New-WhiskeyContext Add-WhiskeyApiKey -Context $context -ID 'PSGalleryApiKey' -Value '901a072f-fe5e-44ec-8546-029ffbec0687' #> [CmdletBinding()] param( [Parameter(Mandatory)] # The context of the build that needs the API key. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The ID of the API key. This should match the ID given in your `whiskey.yml` for the API key ID property of the task that needs it. [String]$ID, [Parameter(Mandatory)] # The value of the API key. Can be a string or a SecureString. $Value ) Set-StrictMode -Version 'Latest' if( $Value -isnot [securestring] ) { $Value = ConvertTo-SecureString -String $Value -AsPlainText -Force } $Context.ApiKeys[$ID] = $Value } function Add-WhiskeyCredential { <# .SYNOPSIS Adds credential to Whiskey's credential store. .DESCRIPTION The `Add-WhiskeyCredential` function adds a credential to Whiskey's credential store. Tasks that need credentials usually have a property where you provide the ID of the credential. You provide Whiskey the value of that credential with this function. For example, if you are publishing a ProGet universal pakcage, your `whiskey.yml` file will look something like this: Publish: - PublishProGetUniversalPackage: Uri: https://proget.example.com FeedName: 'upack' CredentialID: ProgetExampleCom After you create your build's context with `New-WhiskeyContext`, you would then call `Add-WhiskeyCredential` to add the actual credential: $context = New-WhiskeyContext Add-WhiskeyCredential -Context $context -ID 'ProgetExampleCom' -Credential $credential #> [CmdletBinding()] param( [Parameter(Mandatory)] # The context of the build that needs the API key. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The ID of the credential. This should match the ID given in your `whiskey.yml` of credential ID property of the task that needs it. [String]$ID, [Parameter(Mandatory)] # The value of the credential. [pscredential]$Credential ) Set-StrictMode -Version 'Latest' $Context.Credentials[$ID] = $Credential } function Add-WhiskeyTaskDefault { <# .SYNOPSIS Sets default values for task parameters. .DESCRIPTION The `Add-WhiskeyTaskDefault` functions sets default properties for tasks. These defaults are only used if the property is missing from the task in your `whiskey.yml` file, i.e. properties defined in your whiskey.yml file take precedence over task defaults. `TaskName` must be the name of an existing task. Otherwise, `Add-WhiskeyTaskDefault` will throw an terminating error. By default, existing defaults are left in place. To override any existing defaults, use the `-Force`... switch. .EXAMPLE Add-WhiskeyTaskDefault -Context $context -TaskName 'MSBuild' -PropertyName 'Version' -Value 12.0 Demonstrates setting the default value of the `MSBuild` task's `Version` property to `12.0`. .EXAMPLE Add-WhiskeyTaskDefault -Context $context -TaskName 'MSBuild' -PropertyName 'Version' -Value 15.0 -Force Demonstrates overwriting the current default value for `MSBuild` task's `Version` property to `15.0`. #> [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] # The current build context. Use `New-WhiskeyContext` to create context objects. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The name of the task that a default parameter value will be set. [String]$TaskName, [Parameter(Mandatory)] # The name of the task parameter to set a default value for. [String]$PropertyName, [Parameter(Mandatory)] # The default value for the task parameter. $Value, # Overwrite an existing task default with a new value. [switch]$Force ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if (-not ($Context | Get-Member -Name 'TaskDefaults')) { throw 'The given ''Context'' object does not contain a ''TaskDefaults'' property. Create a proper Whiskey context object using the ''New-WhiskeyContext'' function.' } if ($TaskName -notin (Get-WhiskeyTask | Select-Object -ExpandProperty 'Name')) { throw 'Task ''{0}'' does not exist.' -f $TaskName } if ($context.TaskDefaults.ContainsKey($TaskName)) { if ($context.TaskDefaults[$TaskName].ContainsKey($PropertyName) -and -not $Force) { throw 'The ''{0}'' task already contains a default value for the property ''{1}''. Use the ''Force'' parameter to overwrite the current value.' -f $TaskName,$PropertyName } else { $context.TaskDefaults[$TaskName][$PropertyName] = $Value } } else { $context.TaskDefaults[$TaskName] = @{ $PropertyName = $Value } } } function Add-WhiskeyVariable { <# .SYNOPSIS Adds a variable to the current build. .DESCRIPTION The `Add-WhiskeyVariable` adds a variable to the current build. Variables can be used in task properties and at runtime are replaced with their values. Variables syntax is `$(VARIABLE_NAME)`. Variable names are case-insensitive. .EXAMPLE Add-WhiskeyVariable -Context $context -Name 'Timestamp' -Value (Get-Date).ToString('o') Demonstrates how to add a variable. In this example, Whiskey will replace any `$(Timestamp)` variables it finds in any task properties with the value returned by `(Get-Date).ToString('o')`. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The context of the build here you want to add a variable. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The name of the variable. [String]$Name, [Parameter(Mandatory)] [AllowEmptyString()] [Object]$Value ) Set-StrictMode -Version 'Latest' $Context.Variables[$Name] = $Value } function Assert-WhiskeyNodeModulePath { <# .SYNOPSIS Asserts that the path to a Node module directory exists. .DESCRIPTION The `Assert-WhiskeyNodeModulePath` function asserts that a Node module directory exists. If the directory doesn't exist, the function writes an error with details on how to solve the problem. It returns the path if it exists. Otherwise, it returns nothing. This won't fail a build. To fail a build if the path doesn't exist, pass `-ErrorAction Stop`. .EXAMPLE Assert-WhiskeyNodeModulePath -Path $TaskParameter['NspPath'] Demonstrates how to check that a Node module directory exists. .EXAMPLE Assert-WhiskeyNodeModulePath -Path $TaskParameter['NspPath'] -ErrorAction Stop Demonstrates how to fail a build if a Node module directory doesn't exist. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The path to check. [String]$Path, # The path to a command inside the module path. [String]$CommandPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $moduleName = $Path | Split-Path if( -not (Test-Path -Path $Path -PathType Container) ) { Write-WhiskeyError -Message ('Node module ''{0}'' does not exist at ''{1}''. Whiskey or NPM maybe failed to install this module correctly. Clean your build then re-run your build normally. If the problem persists, it might be a task authoring error. Please see the `about_Whiskey_Writing_Tasks` help topic for more information.' -f $moduleName,$Path) return } if( -not $CommandPath ) { return $Path } $commandName = $CommandPath | Split-Path -Leaf $fullCommandPath = Join-Path -Path $Path -ChildPath $CommandPath if( -not (Test-Path -Path $fullCommandPath -PathType Leaf) ) { Write-WhiskeyError -Message ('Node module ''{0}'' does not contain command ''{1}'' at ''{2}''. Whiskey or NPM maybe failed to install this module correctly or that command doesn''t exist in this version of the module. Clean your build then re-run your build normally. If the problem persists, it might be a task authoring error. Please see the `about_Whiskey_Writing_Tasks` help topic for more information.' -f $moduleName,$commandName,$fullCommandPath) return } return $fullCommandPath } function Assert-WhiskeyNodePath { <# .SYNOPSIS Asserts that the path to a node executable exists. .DESCRIPTION The `Assert-WhiskeyNodePath` function asserts that a path to a Node executable exists. If it doesn't, it writes an error with details on how to solve the problem. It returns the path if it exists. Otherwise, it returns nothing. This won't fail a build. To fail a build if the path doesn't exist, pass `-ErrorAction Stop`. .EXAMPLE Assert-WhiskeyNodePath -Path $TaskParameter['NodePath'] Demonstrates how to check that Node exists. .EXAMPLE Assert-WhiskeyNodePath -Path $TaskParameter['NodePath'] -ErrorAction Stop Demonstrates how to fail a build if the path to Node doesn't exist. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The path to check. [String]$Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not (Test-Path -Path $Path -PathType Leaf) ) { Write-WhiskeyError -Message ('Node executable ''{0}'' does not exist. Whiskey maybe failed to install Node correctly. Clean your build then re-run your build normally. If the problem persists, it might be a task authoring error. Please see the `about_Whiskey_Writing_Tasks` help topic for more information.' -f $Path) return } return $Path } function Convert-WhiskeyPathDirectorySeparator { <# .SYNOPSIS Converts the directory separators in a path to the preferred separator for the current platform. .DESCRIPTION The `Convert-WhiskeyPathDirectorySeparator` function uses PowerShell's `Join-Path` cmdlet to convert the directory separator characters in a path to the ones for the current platform. The path does not have to exist. It joins the path with the `.` character, then trims all periods and .EXAMPLE $Path | Convert-WhiskeyPathDirectorySeparator Demonstrates how to use this function by piping paths to it. #> [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [String]$Path ) process { $Path = Join-Path -Path $Path -ChildPath '.' # Take off the '.' period we added. $Path = $Path.TrimEnd('.') # Now remove the extra separator we added. return $Path.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar); } } function ConvertFrom-WhiskeyContext { <# .SYNOPSIS Converts a `Whiskey.Context` into a generic object that can be serialized across platforms. .DESCRIPTION Some tasks need to run in background jobs and need access to Whiskey's context. This function converts a `Whiskey.Context` object into an object that can be serialized by PowerShell across platforms. The object returned by this function can be passed to a `Start-Job` script block. Inside that script block you should import Whiskey and pass the serialized context to `ConvertTo-WhiskeyContext`. $serializableContext = $TaskContext | ConvertFrom-WhiskeyContext $job = Start-Job { Invoke-Command -ScriptBlock { $VerbosePreference = 'SilentlyContinue'; # Or wherever your project keeps Whiskey relative to your task definition. Import-Module -Name (Join-Path -Path $using:PSScriptRoot -ChildPath '..\Whiskey' -Resolve -ErrorAction Stop) } $context = $using:serializableContext | ConvertTo-WhiskeyContext # Run your task } You should create a new serializable context for each job you are running. Whiskey generates a temporary encryption key so it can encrypt/decrypt credentials. Once it decrypts the credentials, it deletes the key from memory. If you use the same context object between jobs, one job will clear the key and other jobs will fail because the key will be gone. .EXAMPLE $TaskContext | ConvertFrom-WhiskeyContext Demonstrates how to call `ConvertFrom-WhiskeyContext`. See the description for a full example. #> [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] # The context to convert. You can pass an existing context via the pipeline. [Whiskey.Context]$Context ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $key = New-Object 'byte[]' (256/8) $rng = New-Object 'Security.Cryptography.RNGCryptoServiceProvider' $rng.GetBytes($key) } process { # PowerShell on Linux/MacOS can't serialize SecureStrings. So, we have to encrypt and serialize them. $serializableCredentials = @{ } foreach( $credentialID in $Context.Credentials.Keys ) { [pscredential]$credential = $Context.Credentials[$credentialID] $serializableCredential = [pscustomobject]@{ UserName = $credential.UserName; Password = ConvertFrom-SecureString -SecureString $credential.Password -Key $key } $serializableCredentials[$credentialID] = $serializableCredential } $serializableApiKeys = @{ } foreach( $apiKeyID in $Context.ApiKeys.Keys ) { [securestring]$apiKey = $Context.ApiKeys[$apiKeyID] $serializableApiKey = ConvertFrom-SecureString -SecureString $apiKey -Key $key $serializableApiKeys[$apiKeyID] = $serializableApiKey } $Context | Select-Object -Property '*' -ExcludeProperty 'Credentials','ApiKeys' | Add-Member -MemberType NoteProperty -Name 'Credentials' -Value $serializableCredentials -PassThru | Add-Member -MemberType NoteProperty -Name 'ApiKeys' -Value $serializableApiKeys -PassThru | Add-Member -MemberType NoteProperty -Name 'CredentialKey' -Value $key.Clone() -PassThru } end { [Array]::Clear($key,0,$key.Length) } } function ConvertFrom-WhiskeyYamlScalar { <# .SYNOPSIS Converts a string that came from a YAML configuation file into a strongly-typed object. .DESCRIPTION The `ConvertFrom-WhiskeyYamlScalar` function converts a string that came from a YAML configuration file into a strongly-typed object according to the parsing rules in the YAML specification. It converts strings into booleans, integers, floating-point numbers, and timestamps. See the YAML specification for examples on how to represent these values in your YAML file. It will convert: * `y`, `yes`, `true`, and `on` to `$true` * `n`, `no`, `false`, and `off` to `$false` * Numbers to `int32` or `int64` types. Numbers can be prefixed with `0x` (for hex), `0b` (for bits), or `0` for octal. * Floating point numbers to `double`, or `single` types. Floating point numbers can be expressed as decimals (`1.5`), or with scientific notation (`6.8523015e+5`). * `~`, `null`, and `` to `$null` * timestamps (e.g. `2001-12-14t21:59:43.10-05:00`) to date If it can't convert a string into a known type, `ConvertFrom-WhiskeyYamlScalar` writes an error. .EXAMPLE $value | ConvertFrom-WhiskeyYamlScalar Demonstrates how to pipe values to `ConvertFrom-WhiskeyYamlScalar`. #> [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [AllowEmptyString()] [AllowNull()] # The object to convert. [String]$InputObject ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( [String]::IsNullOrEmpty($InputObject) -or $InputObject -match '^(~|null)' ) { return $null } if( $InputObject -match '^(y|yes|n|no|true|false|on|off)$' ) { return $InputObject -match '^(y|yes|true|on)$' } # Integer $regex = @' ^( [-+]?0b[0-1_]+ # (base 2) |[-+]?0[0-7_]+ # (base 8) |[-+]?(0|[1-9][0-9_]*) # (base 10) |[-+]?0x[0-9a-fA-F_]+ # (base 16) |[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+ # (base 60) )$ '@ if( [Text.RegularExpressions.Regex]::IsMatch($InputObject, $regex, [Text.RegularExpressions.RegexOptions]::IgnorePatternWhitespace) ) { [int64]$int64 = 0 $value = $InputObject -replace '_','' if( $value -match '^0x' -and [int64]::TryParse(($value -replace '0x',''), [Globalization.NumberStyles]::HexNumber, $null,[ref]$int64)) { } elseif( $value -match '^0b' ) { $int64 = [Convert]::ToInt64(($value -replace ('^0b','')),2) } elseif( $value -match '^0' ) { $int64 = [Convert]::ToInt64($value,8) } elseif( [int64]::TryParse($value,[ref]$int64) ) { } if( $int64 -gt [Int32]::MaxValue ) { return $int64 } return [int32]$int64 } $regex = @' ^( [-+]?([0-9][0-9_]*)?\.[0-9_]*([eE][-+][0-9]+)? # (base 10) |[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\.[0-9_]* # (base 60) |[-+]?\.(inf|Inf|INF) # (infinity) |\.(nan|NaN|NAN) # (not a number) )$ '@ if( [Text.RegularExpressions.Regex]::IsMatch($InputObject, $regex, [Text.RegularExpressions.RegexOptions]::IgnorePatternWhitespace) ) { $value = $InputObject -replace '_','' [Double]$double = 0.0 if( $value -eq '.NaN' ) { return [Double]::NaN } if( $value -match '-\.inf' ) { return [Double]::NegativeInfinity } if( $value -match '\+?.inf' ) { return [Double]::PositiveInfinity } if( [Double]::TryParse($value,[ref]$double) ) { return $double } } $regex = '^\d\d\d\d-\d\d?-\d\d?(([Tt]|[ \t]+)\d\d?\:\d\d\:\d\d(\.\d+)?(Z|\ *[-+]\d\d?(:\d\d)?)?)?$' if( [Text.RegularExpressions.Regex]::IsMatch($InputObject, $regex, [Text.RegularExpressions.RegexOptions]::IgnorePatternWhitespace) ) { [DateTime]$datetime = [DateTime]::MinValue if( ([DateTime]::TryParse(($InputObject -replace 'T',' '),[ref]$datetime)) ) { return $datetime } } Write-WhiskeyError -Message ('Unable to convert scalar value ''{0}''. See http://yaml.org/type/ for documentation on YAML''s scalars.' -f $InputObject) } } function ConvertTo-WhiskeyContext { <# .SYNOPSIS Converts an `Whiskey.Context` returned by `ConvertFrom-WhiskeyContext` back into a `Whiskey.Context` object. .DESCRIPTION Some tasks need to run in background jobs and need access to Whiskey's context. This function converts an object returned by `ConvertFrom-WhiskeyContext` back into a `Whiskey.Context` object. $serializableContext = $TaskContext | ConvertFrom-WhiskeyContext $job = Start-Job { Invoke-Command -ScriptBlock { $VerbosePreference = 'SilentlyContinue'; # Or wherever your project keeps Whiskey relative to your task definition. Import-Module -Name (Join-Path -Path $using:PSScriptRoot -ChildPath '..\Whiskey' -Resolve -ErrorAction Stop) } [Whiskey.Context]$context = $using:serializableContext | ConvertTo-WhiskeyContext # Run your task } .EXAMPLE $serializedContext | ConvertTo-WhiskeyContext Demonstrates how to call `ConvertTo-WhiskeyContext`. See the description for a full example. #> [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] # The context to convert. You can pass an existing context via the pipeline. [Object]$InputObject ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState try { function Sync-ObjectProperty { param( [Parameter(Mandatory)] [Object]$Source, [Parameter(Mandatory)] [Object]$Destination, [String[]]$ExcludeProperty ) $destinationType = $Destination.GetType() $destinationType.DeclaredProperties | Where-Object { $ExcludeProperty -notcontains $_.Name } | Where-Object { $_.GetSetMethod($false) } | Select-Object -ExpandProperty 'Name' | ForEach-Object { Write-WhiskeyDebug ('{0} {1} -> {2}' -f $_,$Destination.$_,$Source.$_) $propertyType = $destinationType.GetProperty($_).PropertyType if( $propertyType.IsSubclassOf([IO.FileSystemInfo]) ) { if( $Source.$_ ) { Write-WhiskeyDebug -Message ('{0} {1} = {2}' -f $propertyType.FullName,$_,$Source.$_) $Destination.$_ = New-Object $propertyType.FullName -ArgumentList $Source.$_.FullName } } else { $Destination.$_ = $Source.$_ } } Write-WhiskeyDebug ('Source -eq $null ? {0}' -f ($Source -eq $null)) if( $Source -ne $null ) { Write-WhiskeyDebug -Message 'Source' Get-Member -InputObject $Source | Out-String | Write-WhiskeyDebug } Write-WhiskeyDebug ('Destination -eq $null ? {0}' -f ($Destination -eq $null)) if( $Destination -ne $null ) { Write-WhiskeyDebug -Message 'Destination' Get-Member -InputObject $Destination | Out-String | Write-WhiskeyDebug } Get-Member -InputObject $Destination -MemberType Property | Where-Object { $ExcludeProperty -notcontains $_.Name } | Where-Object { $name = $_.Name if( -not $name ) { return } $value = $Destination.$name if( $value -eq $null ) { return } Write-WhiskeyDebug ('Destination.{0,-20} -eq $null ? {1}' -f $name,($value -eq $null)) Write-WhiskeyDebug (' .{0,-20} is {1}' -f $name,$value.GetType()) return (Get-Member -InputObject $value -Name 'Keys') -or ($value -is [Collections.IList]) } | ForEach-Object { $propertyName = $_.Name Write-WhiskeyDebug -Message ('{0}.{1} -> {2}.{1}' -f $Source.GetType(),$propertyName,$Destination.GetType()) $destinationObject = $Destination.$propertyName $sourceObject = $source.$propertyName if( (Get-Member -InputObject $destinationObject -Name 'Keys') ) { $keys = $sourceObject.Keys foreach( $key in $keys ) { $value = $sourceObject[$key] Write-WhiskeyDebug (' [{0,-20}] -> {1}' -f $key,$value) $destinationObject[$key] = $sourceObject[$key] } } elseif( $destinationObject -is [Collections.IList] ) { $idx = 0 foreach( $item in $sourceObject ) { Write-WhiskeyDebug(' [{0}] {1}' -f $idx++,$item) $destinationObject.Add($item) } } } } $buildInfo = New-WhiskeyBuildMetadataObject Sync-ObjectProperty -Source $InputObject.BuildMetadata -Destination $buildInfo -Exclude @( 'BuildServer' ) if( $InputObject.BuildMetadata.BuildServer ) { $buildInfo.BuildServer = $InputObject.BuildMetadata.BuildServer } $buildVersion = New-WhiskeyVersionObject Sync-ObjectProperty -Source $InputObject.Version -Destination $buildVersion -ExcludeProperty @( 'SemVer1', 'SemVer2', 'SemVer2NoBuildMetadata' ) if( $InputObject.Version ) { if( $InputObject.Version.SemVer1 ) { $buildVersion.SemVer1 = $InputObject.Version.SemVer1.ToString() } if( $InputObject.Version.SemVer2 ) { $buildVersion.SemVer2 = $InputObject.Version.SemVer2.ToString() } if( $InputObject.Version.SemVer2NoBuildMetadata ) { $buildVersion.SemVer2NoBuildMetadata = $InputObject.Version.SemVer2NoBuildMetadata.ToString() } } [Whiskey.Context]$context = New-WhiskeyContextObject Sync-ObjectProperty -Source $InputObject -Destination $context -ExcludeProperty @( 'BuildMetadata', 'Configuration', 'Version', 'Credentials', 'TaskPaths', 'ApiKeys' ) if( $context.ConfigurationPath ) { $context.Configuration = Import-WhiskeyYaml -Path $context.ConfigurationPath } $context.BuildMetadata = $buildInfo $context.Version = $buildVersion foreach( $credentialID in $InputObject.Credentials.Keys ) { $serializedCredential = $InputObject.Credentials[$credentialID] $username = $serializedCredential.UserName $password = ConvertTo-SecureString -String $serializedCredential.Password -Key $InputObject.CredentialKey [pscredential]$credential = New-Object 'pscredential' $username,$password Add-WhiskeyCredential -Context $context -ID $credentialID -Credential $credential } foreach( $apiKeyID in $InputObject.ApiKeys.Keys ) { $serializedApiKey = $InputObject.ApiKeys[$apiKeyID] $apiKey = ConvertTo-SecureString -String $serializedApiKey -Key $InputObject.CredentialKey Add-WhiskeyApiKey -Context $context -ID $apiKeyID -Value $apiKey } foreach( $path in $InputObject.TaskPaths ) { $context.TaskPaths.Add((New-Object -TypeName 'IO.FileInfo' -ArgumentList $path)) } Write-WhiskeyDebug 'Variables' $context.Variables | ConvertTo-Json -Depth 50 | Write-WhiskeyDebug Write-WhiskeyDebug 'ApiKeys' $context.ApiKeys | ConvertTo-Json -Depth 50 | Write-WhiskeyDebug Write-WhiskeyDebug 'Credentials' $context.Credentials | ConvertTo-Json -Depth 50 | Write-WhiskeyDebug Write-WhiskeyDebug 'TaskDefaults' $context.TaskDefaults | ConvertTo-Json -Depth 50 | Write-WhiskeyDebug Write-WhiskeyDebug 'TaskPaths' $context.TaskPaths | ConvertTo-Json | Write-WhiskeyDebug return $context } finally { # Don't leave the decryption key lying around. [Array]::Clear($InputObject.CredentialKey,0,$InputObject.CredentialKey.Length) } } } function ConvertTo-WhiskeySemanticVersion { <# .SYNOPSIS Converts an object to a semantic version. .DESCRIPTION The `ConvertTo-WhiskeySemanticVersion` function converts strings, numbers, and date/time objects to semantic versions. If the conversion fails, it writes an error and you get nothing back. .EXAMPLE '1.2.3' | ConvertTo-WhiskeySemanticVersion Demonstrates how to convert an object into a semantic version. #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)] # The object to convert to a semantic version. Can be a version string, number, or date/time. [Object] $InputObject ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $InputObject -is [SemVersion.SemanticVersion] ) { return $InputObject } elseif( $InputObject -is [DateTime] ) { $InputObject = '{0}.{1}.{2}' -f $InputObject.Month,$InputObject.Day,$InputObject.Year } elseif( $InputObject -is [Double] ) { $major,$minor = $InputObject.ToString('g') -split '\.' if( -not $minor ) { $minor = '0' } $InputObject = '{0}.{1}.0' -f $major,$minor } elseif( $InputObject -is [int] ) { $InputObject = '{0}.0.0' -f $InputObject } elseif( $InputObject -is [Version] ) { if( $InputObject.Build -le -1 ) { $InputObject = '{0}.0' -f $InputObject } else { $InputObject = $InputObject.ToString() } } [Version]$asVersion = $null [SemVersion.SemanticVersion]$semVersion = $null if( [SemVersion.SemanticVersion]::TryParse($InputObject, [ref]$semVersion) ) { return $semVersion } if( [Version]::TryParse($InputObject, [ref]$asVersion) ) { $major,$minor,$patch = @($asVersion.Major, $asVersion.Minor, $asVersion.Build) | ForEach-Object { if( $_ -eq -1 ) { return 0 } return $_ } return [SemVersion.SemanticVersion]::New($major, $minor, $patch) } [int] $asInt = 0 if( [int]::TryParse($InputObject, [ref]$asInt) ) { return [SemVersion.SemanticVersion]::New($asInt, 0, 0) } $original = $PSBoundParameters['InputObject'] $msg = "Unable to convert ""[$($original.GetType().FullName)] $($original)"" to a semantic version." Write-WhiskeyError -Message $msg } } function ConvertTo-WhiskeyTask { <# .SYNOPSIS Converts an object parsed from a whiskey.yml file into a task name and task parameters. .DESCRIPTION The `ConvertTo-WhiskeyTask` function takes an object parsed from a whiskey.yml file and converts it to a task name and hashtable of parameters and returns both in that order. .EXAMPLE $name,$parameter = ConvertTo-WhiskeyTask -InputObject $parsedTask #> [CmdletBinding()] param( [Parameter(Mandatory)] [AllowNull()] [Object]$InputObject ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $InputObject -is [String] ) { $InputObject @{ } return } elseif( $InputObject -and ($InputObject | Get-Member -Name 'Keys') ) { $taskName = $InputObject.Keys | Select-Object -First 1 $parameter = $InputObject[$taskName] if( -not $parameter ) { $parameter = @{ } } elseif( -not ($parameter | Get-Member -Name 'Keys') ) { $parameter = @{ '' = $parameter } } $taskName $parameter return } # Convert back to YAML to display its invalidness to the user. $builder = New-Object 'YamlDotNet.Serialization.SerializerBuilder' $yamlWriter = New-Object "System.IO.StringWriter" $serializer = $builder.Build() $serializer.Serialize($yamlWriter, $InputObject) $yaml = $yamlWriter.ToString() $yaml = $yaml -split [regex]::Escape([Environment]::NewLine) | Where-Object { @( '...', '---' ) -notcontains $_ } | ForEach-Object { ' {0}' -f $_ } Write-WhiskeyError -Message ('Invalid task YAML:{0} {0}{1}{0}A task must have a name followed by optional parameters, e.g. Build: - Task1 - Task2: Parameter1: Value1 Parameter2: Value2 ' -f [Environment]::NewLine,($yaml -join [Environment]::NewLine)) } function Find-WhiskeyPowerShellModule { <# .SYNOPSIS Searches for a PowerShell module using PowerShellGet to ensure it exists and returns the resulting object from PowerShellGet. .DESCRIPTION The `Find-WhiskeyPowerShellModule` function takes a `Name` of a PowerShell module and uses PowerShellGet's `Find-Module` cmdlet to search for the module. If the module is found, the object from `Find-Module` describing the module is returned. If no module is found, an error is written and nothing is returned. If the module is found in multiple PowerShellGet repositories, only the first one from `Find-Module` is returned. If a `Version` is specified then this function will search for that version of the module from all versions returned from `Find-Module`. If the version cannot be found, an error is written and nothing is returned. `Version` supports wildcard patterns. .EXAMPLE Find-WhiskeyPowerShellModule -Name 'Pester' Demonstrates getting the module info on the latest version of the Pester module. .EXAMPLE Find-WhiskeyPowerShellModule -Name 'Pester' -Version '4.*' Demonstrates getting the module info on the latest '4.X' version of the Pester module. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The name of the PowerShell module. [String]$Name, # The version of the PowerShell module to search for. Must be a three part number, i.e. it must have a MAJOR, # MINOR, and BUILD number. [String]$Version, [Parameter(Mandatory)] # The path to the directory where the PSModules directory should be created. [String]$BuildRoot, # Allow prerelease versions. [switch]$AllowPrerelease ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-WhiskeyDebug '\Find-WhiskeyPowerShellModule\' -Indent function Import { [CmdletBinding()] param( [Parameter(Mandatory)] [String] $Name, [Parameter(Mandatory)] [Version] $MinimumVersion, [Parameter(Mandatory)] [Version] $MaximumVersion ) Write-WhiskeyDebug "Importing module $($Name) for version $($MinimumVersion) - $($MaximumVersion)." $module = Get-Module -Name $Name if( $module ) { if( $module.Version -ge $MinimumVersion -and $module.Version -le $MaximumVersion ) { Write-WhiskeyDebug "Module $($Name) $($module.Version) already imported." return } Write-WhiskeyDebug "Removing module $($Name) $($module.Version)." $module | Remove-Module -Force } $importFailed = $true try { Import-Module -Name $Name ` -MinimumVersion $MinimumVersion ` -MaximumVersion $MaximumVersion ` -Global Write-WhiskeyDebug "Imported module $($Name) $((Get-Module -Name $Name).Version)." $importFailed = $false } finally { if( $importFailed ) { Write-WhiskeyDebug "Exception importing $($Name) module: $($Global:Error[0])" Write-WhiskeyDebug '\PSModulePath\' -Indent $env:PSModulePath -split ([IO.Path]::PathSeparator) | Write-WhiskeyDebug Write-WhiskeyDebug '/PSModulePath/' -Outdent Write-WhiskeyDebug '\Get-Module\' -Indent Get-Module | Out-String | Write-WhiskeyDebug Write-WhiskeyDebug '/Get-Module/' -Outdent Write-WhiskeyDebug '\Get-Module -ListAvailable\' -Indent Get-Module -ListAvailable | Out-String | Write-WhiskeyDebug Write-WhiskeyDebug '/Get-Module -ListAvailable/' -Outdent } } } try { $pwshGetModule = Get-Module -Name 'PowerShellGet' -ErrorAction Ignore if( $pwshGetModule -and $pwshGetModule.Version -lt [Version]'2.0.10' ) { $msg = "The currently installed version of PowerShellGet $($pwshGetModule.Version) is less than the " + 'minimum recommended version 2.0.10. If you run into issues with resolving PowerShell modules, ' + 'please update PowerShellGet to the latest version using the following command: ' + '"Install-Module -Name ''PowerShellGet'' -Force -AllowClobber"' Write-WhiskeyWarning -Message $msg } Register-WhiskeyPSModulePath -PSModulesRoot $BuildRoot $allowPrereleaseArg = Get-AllowPrereleaseArg -CommandName 'Find-Module' -AllowPrerelease:$AllowPrerelease Write-WhiskeyDebug -Message ('{0} {1} ->' -f $Name,$Version) if( $Version ) { $atVersionString = ' at version {0}' -f $Version if( -not [Management.Automation.WildcardPattern]::ContainsWildcardCharacters($version) -and ` [Version]::TryParse($Version,[ref]$null) ) { $tempVersion = [Version]$Version if( $tempVersion -and ($tempVersion.Build -lt 0) ) { $Version = [Version]('{0}.{1}.0' -f $tempVersion.Major, $tempVersion.Minor) } } Write-WhiskeyDebug -Message "Searching for module $($Name) $($Version)." $module = Find-Module -Name $Name -AllVersions @allowPrereleaseArg | Where-Object { $_.Version.ToString() -like $Version } | Sort-Object -Property 'Version' -Descending } else { Write-WhiskeyDebug -Message "Searching for latest version of module $($Name)." $atVersionString = '' $module = Find-Module -Name $Name @allowPrereleaseArg -ErrorAction Ignore } if( -not $module ) { $registeredRepositories = Get-PSRepository | ForEach-Object { ('{0} ({1})' -f $_.Name,$_.SourceLocation) } $registeredRepositories = $registeredRepositories -join ('{0} * ' -f [Environment]::NewLine) Write-WhiskeyError -Message ('Failed to find PowerShell module {0}{1} in any of the registered PowerShell repositories:{2} {2} * {3} {2}' -f $Name, $atVersionString, [Environment]::NewLine, $registeredRepositories) return } $module = $module | Select-Object -First 1 Write-WhiskeyDebug ($module | Format-List | Out-String) Write-WhiskeyDebug -Message ('{0} {1} {2}' -f (' ' * $Name.Length),(' ' * $Version.Length),$module.Version) return $module } finally { Write-WhiskeyDebug '/Find-WhiskeyPowerShellModule/' -Outdent } } function Format-Command { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowNull()] [AllowEmptyString()] [String[]] $ArgumentList ) begin { Set-StrictMode -version 'latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $cmdArgs = [Collections.Generic.List[String]]::New() } process { foreach( $cmdArg in $ArgumentList ) { if( -not $cmdArg ) { continue } if( $cmdArg.Contains(' ') -or $cmdArg.Contains(';') ) { if( $cmdArg.Contains('"') ) { $cmdArg = $cmdArg.Replace('"', '""') } $cmdArg = """$($cmdArg)""" } $cmdArgs.Add($cmdArg) } } end { return $cmdArgs -join ' ' } } function Format-Stopwatch { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [Diagnostics.Stopwatch]$Stopwatch ) process { $duration = $Stopwatch.Elapsed "{0,2}m{1:00}s" -f $duration.TotalMinutes.ToUInt32($null), $duration.Seconds } } function Get-AllowPrereleaseArg { <# .SYNOPSIS Return a hashtable that can be splatted for a command that can or can't have an AllowPrerelease parameter. .DESCRIPTION Whiskey has to support older versions of PowerShellGet and PackageManagement. Some of these older versions don't have support for the `AllowPrerelease` switch and some of them have switches that function to allow prereleases, but the parameter name is different. This function determines if the function supports `AllowPrerelease` or not, and if it does *and* this function's `AllowPrerelease` switch is set, returns a hashtable with an `AllowPrerelease` key (or whatever the parameter name is for that function) whose value is set to `$true`. Otherwise, it returns an empty hashtable. #> [CmdletBinding()] param( [Parameter(Mandatory)] [String] $CommandName, [switch] $AllowPrerelease ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $AllowPrerelease ) { return @{} } $cmd = Get-Command -Name $CommandName -ParameterName 'AllowPrerelease*' -ErrorAction Ignore if( $cmd ) { $allowPrereleaseArg = @{} $cmd.Parameters.Keys | Where-Object { $_ -like 'AllowPrerelease*' } | ForEach-Object { $allowPrereleaseArg[$cmd.Parameters[$_].Name] = $true } return $allowPrereleaseArg } return @{} } function Get-MSBuild { [CmdletBinding()] param( ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState function Resolve-MSBuildToolsPath { param( [Microsoft.Win32.RegistryKey]$Key ) $toolsPath = Get-ItemProperty -Path $Key.PSPath -Name 'MSBuildToolsPath' -ErrorAction Ignore | Select-Object -ExpandProperty 'MSBuildToolsPath' -ErrorAction Ignore if( -not $toolsPath ) { $msg = "$($indent)Skipping registry key ""$($Key | Convert-Path)"": key value ""MSBuildToolsPath"" " + 'doesn''t exist.' Write-WhiskeyVerbose -Message $msg return '' } $path = Join-Path -Path $toolsPath -ChildPath 'MSBuild.exe' if( (Test-Path -Path $path -PathType Leaf) ) { return $path } $msg = "$($indent)Skipping registry key ""$($Key | Convert-Path)"": key value ""MSBuildToolsPath"" " + "is path ""$($path)"", which doesn't exist." Write-WhiskeyVerbose -Message $msg return '' } filter Test-Version { param( [Parameter(Mandatory,ValueFromPipeline)] $InputObject ) [Version]$version = $null [Version]::TryParse($InputObject,[ref]$version) } Write-WhiskeyVerbose '[Get-MSBuild]' $indent = ' ' $toolsVersionRegPath = 'HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions' $toolsVersionRegPath32 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\MSBuild\ToolsVersions' $tools32Exists = Test-Path -Path $toolsVersionRegPath32 -PathType Container foreach( $key in (Get-ChildItem -Path $toolsVersionRegPath) ) { $name = $key.Name | Split-Path -Leaf if( -not ($name | Test-Version) ) { $msg = "$($preindentfix)Skipping registry key ""$($key | Convert-Path)"": name isn't a version number." Write-WhiskeyVerbose -Message $msg continue } $msbuildPath = Resolve-MSBuildToolsPath -Key $key if( -not $msbuildPath ) { continue } $msbuildPath32 = $msbuildPath if( $tools32Exists ) { $key32 = Get-ChildItem -Path $toolsVersionRegPath32 | Where-Object { ($_.Name | Split-Path -Leaf) -eq $name } if( $key32 ) { $msbuildPath32 = Resolve-MSBuildToolsPath -Key $key32 } else { $msbuildPath32 = '' } } Write-WhiskeyVerbose ("$($indent)Found MSBuild $($name) at ""$($msbuildPath)"".") [pscustomobject]@{ Name = $name; Version = [Version]$name; Path = $msbuildPath; Path32 = $msbuildPath32; PathArm64 = ''; } } foreach( $instance in (Get-VSSetupInstance) ) { $msbuildRoot = Join-Path -Path $instance.InstallationPath -ChildPath 'MSBuild' if( -not (Test-Path -Path $msbuildRoot -PathType Container) ) { $msg = "$($indent)Skipping $($instance.DisplayName): its MSBuild directory ""$($msbuildRoot)"" doesn''t " + 'exist.' Write-WhiskeyVerbose -Message $msg continue } $path32 = Join-Path -Path $msbuildRoot -ChildPath '*\Bin\MSBuild.exe' -Resolve -ErrorAction Ignore if( -not $path32 ) { $msg = "$($indent)Skipping $($instance.DisplayName): " + """$(Join-Path -Path $msbuildRoot -ChildPath '*\Bin\MSBuild.exe')"" doesn't exist." """$($msbuildRoot)"" doesn''t exist." Write-WhiskeyVerbose -Message $msg continue } $msbuildRoot = $path32 | Split-Path | Split-Path $path = Join-Path -Path $msbuildRoot -ChildPath 'Bin\amd64\MSBuild.exe' -Resolve -ErrorAction Ignore $pathArm64 = Join-Path -Path $msbuildRoot -ChildPath 'Bin\arm64\MSBuild.exe' -Resolve -ErrorAction Ignore if( -not $path -and $path32 ) { $path = $path32 } if( -not $path ) { $msg = "$($indent)Skipping $($instance.DisplayName) $($instance.InstallationVersion): " + """$(Join-Path -Path $msbuildRoot -ChildPath 'Bin\amd64\MSBuild.exe')"" doesn't exist." """$($msbuildRoot)"" doesn''t exist." Write-WhiskeyVerbose -Message $msg continue } $majorVersion = Get-Item -Path $path | Select-Object -ExpandProperty 'VersionInfo' | Select-Object -ExpandProperty 'ProductMajorPart' $majorMinor = '{0}.0' -f $majorVersion Write-WhiskeyVerbose ("$($indent)Found MSBuild $($majorMinor) at ""$($path)"".") [pscustomobject]@{ Name = $majorMinor; Version = [Version]$majorMinor; Path = $path; Path32 = $path32; PathArm64 = $pathArm64; } | Write-Output } Write-WhiskeyVerbose '[Get-MSBuild]' } function Get-TaskArgument { [CmdletBinding()] param( # The name of the command. [Parameter(Mandatory)] [String] $Name, # The properties from the tasks's YAML. [Parameter(Mandatory)] [hashtable] $Property, # The current context. [Parameter(Mandatory)] [Whiskey.Context] $Context ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState # Parameters of the actual command. $cmdParameters = Get-Command -Name $task.CommandName | Select-Object -ExpandProperty 'Parameters' # Parameters to pass to the command. $taskArgs = @{ } [Management.Automation.ParameterMetadata]$cmdParameter = $null foreach( $cmdParameter in $cmdParameters.Values ) { $propertyName = $cmdParameter.Name $value = $Property[$propertyName] if( -not $value ) { foreach( $aliasName in $cmdParameter.Aliases ) { $value = $Property[$aliasName] if( $value ) { Write-WhiskeyWarning -Context $Context -Message ('Property "{0}" is deprecated. Rename to "{1}" instead.' -f $aliasName,$propertyName) break } } } # PowerShell can't implicitly convert strings to bool/switch values so we have to do it. if( $cmdParameter.ParameterType -eq [switch] -or $cmdParameter.ParameterType -eq [bool] ) { $value = $value | ConvertFrom-WhiskeyYamlScalar } [Whiskey.Tasks.ParameterValueFromVariableAttribute]$valueFromVariableAttr = $cmdParameter.Attributes | Where-Object { $_ -is [Whiskey.Tasks.ParameterValueFromVariableAttribute] } if( $valueFromVariableAttr ) { $value = Resolve-WhiskeyVariable -InputObject ('$({0})' -f $valueFromVariableAttr.VariableName) ` -Context $Context } [Whiskey.Tasks.ValidatePathAttribute]$validatePathAttribute = $cmdParameter.Attributes | Where-Object { $_ -is [Whiskey.Tasks.ValidatePathAttribute] } if( $validatePathAttribute ) { $params = @{ } $params['CmdParameter'] = $cmdParameter $params['ValidatePathAttribute'] = $validatePathAttribute $value = $value | Resolve-WhiskeyTaskPath -TaskContext $Context -TaskParameter $Property @params } # If the user didn't provide a value and we couldn't find one, don't pass anything. if( -not $Property.ContainsKey($propertyName) -and -not $value ) { continue } $taskArgs[$propertyName] = $value } foreach( $name in @( 'TaskContext', 'Context' ) ) { if( $cmdParameters.ContainsKey($name) ) { $taskArgs[$name] = $Context } } foreach( $name in @( 'TaskParameter', 'Parameter' ) ) { if( $cmdParameters.ContainsKey($name) ) { $taskArgs[$name] = $Property } } return $taskArgs } function Get-WhiskeyApiKey { <# .SYNOPSIS Gets an API key from the Whiskey API key store. .DESCRIPTION The `Get-WhiskeyApiKey` function returns an API key from Whiskey's API key store. If the API key doesn't exist, the current build stops (i.e. a terminating exception is thrown). Credentials are identified by an ID that you create. Credentials are added using `Add-WhiskeyCredential`. Credentials are used by tasks. You specify the credential's ID in tasks section of the `whiskey.yml` file. See the documentation for each task for more details. API keys are identified by an ID that you create. API keys are added using `Add-WhiskeyApiKey`. API keys are used by tasks. You specify the API keys' ID in the task's section of the `whiskey.yml` file. See the documentation for each task for more details. .EXAMPLE Get-WhiskeyApiKey -Context $context -ID 'nuget.org' -PropertyName 'ApiKeyID' Demonstrates how to get an API key. IN this case, retrieves the API key that was added with the ID `nuget.org`. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The current build context. Use `New-WhiskeyContext` to create context objects. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The ID of the API key. You make this up. [String]$ID, [Parameter(Mandatory)] # The property name in the task that needs this API key. Used in error messages to help users pinpoint what task and property might be misconfigured. [String]$PropertyName ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $Context.ApiKeys.ContainsKey($ID) ) { Stop-WhiskeyTask -TaskContext $Context ` -Message ('API key ''{0}'' does not exist in Whiskey''s API key store. Use the `Add-WhiskeyApiKey` function to add this API key, e.g. `Add-WhiskeyApiKey -Context $context -ID ''{0}'' -Value $apikey`.' -f $ID) ` -PropertyName $PropertyName return } $secureString = $Context.ApiKeys[$ID] $convertFromSecureString = Get-Command -Name 'ConvertFrom-SecureString' if( $convertFromSecureString.Parameters.ContainsKey('AsPlainText') ) { ConvertFrom-SecureString -SecureString $secureString -AsPlainText } else { $stringPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($stringPtr) } } function Get-WhiskeyBuildMetadata { <# SYNOPSIS Gets metadata about the current build. .DESCRIPTION The `Get-WhiskeyBuildMetadata` function gets information about the current build. It is exists to hide what CI server the current build is running under. It returns an object with the following properties: * `ScmUri`: the URI to the source control repository used in this build. * `BuildNumber`: the build number of the current build. This is the incrementing number most CI servers used to identify a build of a specific job. * `BuildID`: this unique identifier for this build. Usually, this is used by CI servers to distinguish this build from builds across all jobs. * `ScmCommitID`: the full ID of the commit that is being built. * `ScmBranch`: the branch name of the commit that is being built. * `JobName`: the name of the job that is running the build. * `BuildUri`: the URI to this build's results. #> [CmdletBinding()] param( ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState function Get-EnvironmentVariable { param( $Name ) Get-Item -Path ('env:{0}' -f $Name) -ErrorAction Ignore | Select-Object -ExpandProperty 'Value' } $buildInfo = New-WhiskeyBuildMetadataObject if( (Test-Path -Path 'env:JENKINS_URL') ) { $buildInfo.BuildNumber = Get-EnvironmentVariable 'BUILD_NUMBER' $buildInfo.BuildID = Get-EnvironmentVariable 'BUILD_TAG' $buildInfo.BuildUri = Get-EnvironmentVariable 'BUILD_URL' $buildInfo.JobName = Get-EnvironmentVariable 'JOB_NAME' $buildInfo.JobUri = Get-EnvironmentVariable 'JOB_URL' $buildInfo.ScmUri = Get-EnvironmentVariable 'GIT_URL' $buildInfo.ScmCommitID = Get-EnvironmentVariable 'GIT_COMMIT' $buildInfo.ScmBranch = Get-EnvironmentVariable 'GIT_BRANCH' $buildInfo.ScmBranch = $buildInfo.ScmBranch -replace '^origin/','' $buildInfo.BuildServer = [Whiskey.BuildServer]::Jenkins if( (Test-Path -Path 'env:CHANGE_BRANCH') ) { $buildInfo.IsPullRequest = $true $buildInfo.ScmSourceBranch = Get-EnvironmentVariable 'CHANGE_BRANCH' $buildInfo.ScmSourceBranch = $buildInfo.ScmSourceBranch -replace '^origin/','' $buildInfo.ScmSourceCommitID = Get-EnvironmentVariable 'GIT_COMMIT' } } elseif( (Test-Path -Path 'env:APPVEYOR') ) { $buildInfo.BuildNumber = Get-EnvironmentVariable 'APPVEYOR_BUILD_NUMBER' $buildInfo.BuildID = Get-EnvironmentVariable 'APPVEYOR_BUILD_ID' $accountName = Get-EnvironmentVariable 'APPVEYOR_ACCOUNT_NAME' $projectSlug = Get-EnvironmentVariable 'APPVEYOR_PROJECT_SLUG' $projectUri = 'https://ci.appveyor.com/project/{0}/{1}' -f $accountName,$projectSlug $buildVersion = Get-EnvironmentVariable 'APPVEYOR_BUILD_VERSION' $buildUri = '{0}/build/{1}' -f $projectUri,$buildVersion $buildInfo.BuildUri = $buildUri $buildInfo.JobName = Get-EnvironmentVariable 'APPVEYOR_PROJECT_NAME' $buildInfo.JobUri = $projectUri $baseUri = '' switch( (Get-EnvironmentVariable 'APPVEYOR_REPO_PROVIDER') ) { 'gitHub' { $baseUri = 'https://github.com' } default { $msg = "Unsupported AppVeyor source control provider ""$($_)"". If you'd like us to add support for " + 'this provider, please submit a new issue at ' + 'https://github.com/webmd-health-services/Whiskey/issues. Copy/paste your environment ' + 'variables from this build''s output into your issue.' Write-WhiskeyError -Message $msg } } $repoName = Get-EnvironmentVariable 'APPVEYOR_REPO_NAME' $buildInfo.ScmUri = '{0}/{1}.git' -f $baseUri,$repoName $buildInfo.ScmCommitID = Get-EnvironmentVariable 'APPVEYOR_REPO_COMMIT' $buildInfo.ScmBranch = Get-EnvironmentVariable 'APPVEYOR_REPO_BRANCH' if( (Test-Path -Path 'env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH') ) { $buildInfo.IsPullRequest = $true $buildInfo.ScmSourceBranch = Get-EnvironmentVariable 'APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH' $buildInfo.ScmSourceCommitID = Get-EnvironmentVariable 'APPVEYOR_PULL_REQUEST_HEAD_COMMIT' } $buildInfo.BuildServer = [Whiskey.BuildServer]::AppVeyor } elseif( (Test-Path -Path 'env:TEAMCITY_BUILD_PROPERTIES_FILE') ) { function Import-TeamCityProperty { [OutputType([hashtable])] param( $Path ) $properties = @{ } Get-Content -Path $Path | Where-Object { $_ -match '^([^=]+)=(.*)$' } | ForEach-Object { $properties[$Matches[1]] = $Matches[2] -replace '\\(.)','$1' } $properties } $buildInfo.BuildNumber = Get-EnvironmentVariable 'BUILD_NUMBER' $buildInfo.ScmCommitID = Get-EnvironmentVariable 'BUILD_VCS_NUMBER' $buildPropertiesPath = Get-EnvironmentVariable 'TEAMCITY_BUILD_PROPERTIES_FILE' $buildProperties = Import-TeamCityProperty -Path $buildPropertiesPath $buildInfo.BuildID = $buildProperties['teamcity.build.id'] $buildInfo.JobName = $buildProperties['teamcity.buildType.id'] $configProperties = Import-TeamCityProperty -Path $buildProperties['teamcity.configuration.properties.file'] $buildInfo.ScmBranch = $configProperties['teamcity.build.branch'] -replace '^refs/heads/','' $buildInfo.ScmUri = $configProperties['vcsroot.url'] $buildInfo.BuildServer = [Whiskey.BuildServer]::TeamCity $serverUri = $configProperties['teamcity.serverUrl'] $buildInfo.JobUri = '{0}/viewType.html?buildTypeId={1}' -f $serverUri,$buildInfo.JobName $buildInfo.BuildUri = '{0}/viewLog.html?buildId={1}&buildTypeId={2}' -f $serverUri,$buildInfo.BuildID,$buildInfo.JobName } return $buildInfo } function Get-WhiskeyContext { [CmdletBinding()] [OutputType([Whiskey.Context])] param( ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState [Management.Automation.CallStackFrame[]]$callstack = Get-PSCallStack # Skip myself. for( $idx = 1; $idx -lt $callstack.Length; ++$idx ) { $frame = $callstack[$idx] $invokeInfo = $frame.InvocationInfo if( -not $invokeInfo.MyCommand -or $invokeInfo.MyCommand.ModuleName -ne 'Whiskey' ) { # Nice try! continue } $frameParams = $invokeInfo.BoundParameters foreach( $parameterName in $frameParams.Keys ) { $value = $frameParams[$parameterName] if( $null -ne $value -and $value -is [Whiskey.Context] ) { return $value } } } } function Get-WhiskeyCredential { <# .SYNOPSIS Gets a credential from the Whiskey credential store. .DESCRIPTION The `Get-WhiskeyCredential` function returns a credential from Whiskey's credential store. If the credential doesn't exist, the current build stops (i.e. a terminating exception is thrown). Credentials are identified by an ID that you create. Credentials are added using `Add-WhiskeyCredential`. Credentials are used by tasks. You specify the credential's ID in the task's section of the `whiskey.yml` file. See the documentation for each task for more details. .EXAMPLE Get-WhiskeyCredential -Context $context -ID 'bitbucketserver.example.com' -PropertyName 'CredentialID' Demonstrates how to get a credential. IN this case, retrieves the credential that was added with the ID `bitbucketserver.example.com`. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The current build context. Use `New-WhiskeyContext` to create context objects. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The ID of the credential. You make this up. [String]$ID, [Parameter(Mandatory)] # The property name in the task that needs this credential. Used in error messages to help users pinpoint what task and property might be misconfigured. [String]$PropertyName, # INTERNAL. DO NOT USE. [String]$PropertyDescription ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $Context.Credentials.ContainsKey($ID) ) { $propertyDescriptionParam = @{ } if( $PropertyDescription ) { $propertyDescriptionParam['PropertyDescription'] = $PropertyDescription } Stop-WhiskeyTask -TaskContext $Context ` -Message ('Credential "{0}" does not exist in Whiskey''s credential store. Use the `Add-WhiskeyCredential` function to add this credential, e.g. `Add-WhiskeyCredential -Context $context -ID ''{0}'' -Credential $credential`.' -f $ID) ` @propertyDescriptionParam return } return $Context.Credentials[$ID] } function Get-WhiskeyMSBuildConfiguration { <# .SYNOPSIS Gets the configuration to use when running any MSBuild-based task/tool. .DESCRIPTION The `Get-WhiskeyMSBuildConfiguration` function gets the configuration to use when running any MSBuild-based task/tool (e.g. the `MSBuild`, `DotNetBuild`, `DotNetPublish`, etc.). By default, the value is `Debug` when the build is being run by a developer and `Release` when run by a build server. Use `Set-WhiskeyMSBuildConfiguration` to change the current configuration. .EXAMPLE Get-WhiskeyMSBuildConfiguration -Context $Context Gets the configuration to use when runinng an MSBuild-based task/tool #> [CmdletBinding()] param( [Parameter(Mandatory)] # The context of the current build. [Whiskey.Context]$Context ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $Context.MSBuildConfiguration ) { $configuration = 'Debug' if( $Context.ByBuildServer ) { $configuration = 'Release' } Set-WhiskeyMSBuildConfiguration -Context $Context -Value $configuration } return $Context.MSBuildConfiguration } function Get-WhiskeyPSModule { <# .SYNOPSIS Get's module information, with priority given to modules saved in Whiskey's PSModules directory. .DESCRIPTION The `Get-WhiskeyPSModule` function return a PowerShell module information object for a module. It returns the same object returned by PowerShell's `Get-Module` cmdlet. Pass the name of the module to the `Name` parameter. Pass the path to the directory that contains Whiskey's "PSModules" directory (this is usually the build root). The function uses `Get-Module` to find the requested module and return its metadata information. The function validates the module's manifest to ensure the module could be imported. Modules that would fail to be imported are not returned. If multiple versions of a module exist, the latest version is returned. If you want a specific version, pass the version to the `Version` parameter. The `Get-WhiskeyPSModule` will return the latest version that matches the version. Wildcards are supported. If no modules exist, nothing is returned and no errors are written. This function adds the PSModules directory to the `PSModulePath` environment variable. If this path is in the build root, it will be removed when a build is done. This funcation adds a `ManifestPath` property to the return object that is the path to the module's .psd1 file. .LINK Find-WhiskeyPSModule .EXAMPLE Get-WhiskeyPSModule -Name Pester -PSModulesRoot $Context.BuildRoot Demonstrates how to call `Get-WhiskeyPSModule` to get module information. In this case, the function will return the latest version of the `Pester` module, and will include the PSModules path in the build root. .EXAMPLE Get-WhiskeyPSModule -Name Pester -PSModulesRoot $Context.BuildRoot -Version '4.*' Demonstrates how to call `Get-WhiskeyPSModule` to get module information for a specific version of a module. In this example, the function will return the latest 4.x version of the `Pester` module, and will include the PSModules path in the build root. #> [CmdletBinding()] param( [Parameter(Mandatory)] [String]$Name, [String]$Version, [Parameter(Mandatory)] $PSModulesRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-WhiskeyDebug '\Get-WhiskeyPSModule\' -Indent Register-WhiskeyPSModulePath -PSModulesRoot $PSModulesRoot try { Write-WhiskeyDebug '\env:PSModulePath\' -Indent $env:PSModulePath -split [IO.Path]::PathSeparator | Write-WhiskeyDebug Write-WhiskeyDebug '/env:PSModulePath/' -Outdent Write-WhiskeyDebug "\Get-Module -Name ''$($Name)'' -ListAvailable\" -Indent $modules = Get-Module -Name $Name -ListAvailable -ErrorAction Ignore $modules | Out-String | Write-WhiskeyDebug Write-WhiskeyDebug "/Get-Module -Name ''$($Name)'' -ListAvailable/" -Outdent $modules | Where-Object { if( -not $Version ) { return $true } $moduleInfo = $_ $moduleVersion = $moduleInfo.Version $prerelease = '' if( ($moduleInfo | Get-Member 'PreRelease') ) { $prerelease = $moduleInfo.PreRelease } else { $privateData = $moduleInfo.PrivateData if( $privateData ) { $psdata = $privateData['PSData'] if( $psdata ) { $prerelease = $psdata['Prerelease'] } } } if( $prerelease ) { $moduleVersion = "$($moduleVersion)-$($prerelease)" } $msg = "Checking if $($moduleInfo.Name) module's version $($moduleVersion) is like ""$($Version)""." Write-WhiskeyDebug -Message $msg return $moduleVersion -like $Version } | Add-Member -Name 'ManifestPath' ` -MemberType ScriptProperty ` -Value { return Join-Path -Path ($_.Path | Split-Path) -ChildPath "$($_.Name).psd1" } ` -Force ` -PassThru | Where-Object { $module = $_ # Make sure there's a valid module there. $numErrorsBefore = $Global:Error.Count $manifest = $null $debugMsg = "Module $($module.Name) $($module.Version) ($($module.ManifestPath)) has " try { $manifest = Test-ModuleManifest -Path $module.ManifestPath -ErrorAction Ignore -WarningAction Ignore Write-WhiskeyDebug -Message ("$($debugMsg)a valid manifest.") } catch { Write-WhiskeyDebug -Message ("$($debugMsg)an invalid manifest: $($_).") $numErrorsToRemove = $Global:Error.Count - $numErrorsBefore for( $idx = 0; $idx -lt $numErrorsToRemove; ++$idx ) { $Global:Error.RemoveAt(0) } } if( -not $manifest ) { return $false } return $true } | # Get the highest versioned module in the order in which they appear in the PSModulePath environment variable. Group-Object -Property 'Version' | Sort-Object -Property { [Version]$_.Name } -Descending | Select-Object -First 1 | Select-Object -ExpandProperty 'Group' | Select-Object -First 1 } finally { Write-WhiskeyDebug '/Get-WhiskeyPSModule/' -Outdent } } function Get-WhiskeyPSModulePath { [CmdletBinding()] param( [Parameter(Mandatory)] [String]$PSModulesRoot, [switch]$Create ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-WhiskeyDebug '\Get-WhiskeyPSModulePath\' -Indent try { $path = Join-Path -Path $PSModulesRoot -ChildPath 'PSModules' | Write-Output if( $Create -and -not (Test-Path -Path $path) ) { New-Item -Path $path -ItemType 'Directory' | Out-Null } return $path } finally { Write-WhiskeyDebug '/Get-WhiskeyPSModulePath/' -Outdent } } function Get-WhiskeyTask { <# .SYNOPSIS Returns a list of available Whiskey tasks. .DESCRIPTION The `Get-WhiskeyTask` function returns a list of all available Whiskey tasks. Obsolete tasks are not returned. If you also want obsolete tasks returned, use the `-Force` switch. .EXAMPLE Get-WhiskeyTask Demonstrates how to get a list of all non-obsolete Whiskey tasks. .EXAMPLE Get-WhiskeyTask -Force Demonstrates how to get a list of all Whiskey tasks, including those that are obsolete. #> [CmdLetBinding()] [OutputType([Whiskey.TaskAttribute])] param( # Return tasks that are obsolete. Otherwise, no obsolete tasks are returned. [switch]$Force ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState [Management.Automation.FunctionInfo]$functionInfo = $null; foreach ($functionInfo in (Get-ChildItem -Path 'Function:\')) { $functionInfo.ScriptBlock.Attributes | Where-Object { $_ -is [Whiskey.TaskAttribute] } | ForEach-Object { $_.CommandName = $functionInfo.Name $_ } | Where-Object { if( $Force ) { $true } return -not $_.Obsolete } } } function Get-WhiskeyTempPath { [CmdletBinding()] param( [Object] $Context, [String] $Name ) $tempPath = [IO.Path]::GetTempPath() if (-not $Context) { $Context = Get-WhiskeyContext } if ($Context) { $tempPath = $Context.Temp.FullName } if ($Name) { $tempPath = Join-Path -Path $tempPath -ChildPath $Name } if (-not (Test-Path -Path $tempPath)) { New-Item -Path $tempPath -ItemType Directory -Force | Out-Null } return $tempPath } function Import-WhiskeyPowerShellModule { <# .SYNOPSIS Imports a PowerShell module. .DESCRIPTION The `Import-WhiskeyPowerShellModule` function imports a PowerShell module that is needed/used by a Whiskey task. Since Whiskey tasks all run in the module's scope, the imported modules are imported into the global scope. If the module is currently loaded but is at a different version than requested, that module is removed first, and the correct version is imported. If the module isn't installed (or if the requested version of the module isn't installed), you'll get an error. Use the `Install-WhiskeyPowerShellModule` to install the module. If a task needs a module, use the `[Whiskey.RequiresPowerShellModule]` task attribute. .EXAMPLE Import-WhiskeyPowerShellModule -Name 'Zip' -PSModulesRoot $buildRoot Demonstrates how to use this method to import a module that is installed in a global module location or in the current build's PSModules directory (usually in the build root directory). The latest/newest installed version is imported. .EXAMPLE Import-WhiskeyPowerShellModule -Name 'Zip' -Version '0.2.0' -PSModulesRoot $buildRoot Demonstrates how to use this method to import a specific version of a module that is installed in a global module location or in the current build's PSModules directory (usually in the build root directory). The latest/newest installed version is imported. #> [CmdletBinding()] param( # The module names to import. [Parameter(Mandatory)] [String] $Name, # The version of the module to import. [String] $Version, # The path to the build root, where the PSModules directory can be found. Must be included to import a locally installed module. [Parameter(Mandatory)] [String] $PSModulesRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $numErrorsBefore = $Global:Error.Count $foundModule = $false try { $foundModule = & { $VerbosePreference = 'SilentlyContinue' $module = Get-WhiskeyPSModule -Name $Name -Version $Version -PSModulesRoot $PSModulesRoot if( -not $module ) { return $false } $loadedModules = Get-Module -Name $Name $loadedModules | Where-Object 'Version' -ne $module.Version | Remove-Module -Verbose:$false -WhatIf:$false -Force if( ($loadedModules | Where-Object 'Version' -eq $module.Version) ) { Write-WhiskeyDebug -Message ("Module $($Name) $($module.Version) already loaded.") return $true } $module | Import-Module -Global -ErrorAction Stop -Verbose:$false -WarningAction 'Ignore' return $true } 4> $null } finally { # Some modules (...cough...PowerShellGet...cough...) write silent errors during import. This causes our # tests to fail. I know this is a little extreme. $numToRemove = $Global:Error.Count - $numErrorsBefore for( $idx = 0; $idx -lt $numToRemove; $idx++ ) { $Global:Error.RemoveAt(0) } } if( -not $foundModule ) { $versionDesc = '' if( $Version ) { $versionDesc = " $($Version)" } $msg = "Unable to import module ""$($Name)""$($versionDesc): that module isn't installed. To install a module, " + 'use the "GetPowerShellModule" task.' Write-WhiskeyError -Message $msg } } function Import-WhiskeyYaml { [CmdletBinding()] param( [Parameter(Mandatory,ParameterSetName='FromFile')] [String]$Path, [Parameter(Mandatory,ParameterSetName='FromString')] [String]$Yaml ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $PSCmdlet.ParameterSetName -eq 'FromFile' ) { $Yaml = Get-Content -Path $Path -Raw } $builder = New-Object 'YamlDotNet.Serialization.DeserializerBuilder' $deserializer = $builder.Build() $reader = New-Object 'IO.StringReader' $Yaml $config = @{} try { $config = $deserializer.Deserialize( $reader ) } catch { if( $PSCmdlet.ParameterSetName -eq 'FromFile' ) { Write-WhiskeyError "Whiskey configuration file ""$($Path)"" cannot be parsed: $($_)." -ErrorAction Stop } else { Write-WhiskeyError "YAML cannot be parsed: $($_)$([Environment]::NewLine * 2)$($Yaml)" -ErrorAction Stop } } finally { $reader.Close() } if( -not $config ) { $config = @{} } if( $config -is [String] ) { $config = @{ $config = '' } } return $config } function Install-WhiskeyDotNetSdk { <# .SYNOPSIS Installs the .NET SDK. .DESCRIPTION The `Install-WhiskeyDotNetSdk` function installs the .NET SDK. It uses the `dotnet-install.ps1` and `dotnet-install.sh` scripts—provided and supported by Microsoft—on Windows and Linux/macOS, respectively. Any output from the install scripts is written instead to PowerShell's information stream. The function returns the path to the dotnet command. If a `dotnet` tool is already installed and availble, `Install-WhiskeyDotNetSdk` inspects the contents of its installation folder to determine if the version of the SDK is installed globally (it looks for a "sdk\$VERSION" directory where the dotnet command is. If the SDK is installed, the path to the global dotnet command is returned. .EXAMPLE Install-WhiskeyDotNetSdk -InstallRoot 'C:\Build\.dotnet' -Version '2.1.4' Demonstrates installing .NET Core SDK version 2.1.4 to the 'C:\Build\.dotnet' directory. After install the function will return the path 'C:\Build\.dotnet\dotnet.exe'. #> [CmdletBinding()] param( # Directory where the .NET Core SDK will be installed. [Parameter(Mandatory)] [String] $InstallRoot, # Version of the .NET Core SDK to install. [Parameter(Mandatory)] [String] $Version ) Set-StrictMode -version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotnetPaths = Get-Command -Name 'dotnet' -All -ErrorAction Ignore | Select-Object -ExpandProperty 'Source' if( $dotnetPaths ) { $msg = "Checking for installed .NET SDK $($Version)." Write-WhiskeyVerbose -Message $msg foreach( $dotnetPath in $dotnetPaths ) { $sdkPath = Join-Path -Path ($dotnetPath | Split-Path -Parent) -ChildPath ('sdk\{0}' -f $Version) if (Test-Path -Path $sdkPath -PathType Container) { $msg = "Found .NET SDK $($Version) at ""$($sdkPath)""." Write-WhiskeyVerbose -Message $msg return $dotnetPath } } } $InstallRoot = $InstallRoot | Resolve-WhiskeyRelativePath $msg = "Installing .NET SDK $($Version) to ""$($InstallRoot)""." Write-WhiskeyInfo -Message $msg if( -not (Test-Path -Path $InstallRoot) ) { New-Item -Path $InstallRoot -ItemType 'Directory' | Out-Null } $verboseParam = @{} [String[]] $displayArgs = & { if( -not $IsWindows ) { '' } '-InstallDir' $InstallRoot '-Version' $Version if( $IsWindows ) { '-NoPath' if( $VerbosePreference -eq 'Continue' ) { '-Verbose' $verboseParam['Verbose'] = $true } } } # Both scripts handle if the .NET SDK is installed or not. if( $IsWindows ) { $cmdName = 'dotnet.exe' $dotnetInstallPath = Join-Path -Path $whiskeyBinPath -ChildPath 'dotnet-install.ps1' | Resolve-WhiskeyRelativePath $ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue Write-WhiskeyCommand -Path $dotnetInstallPath -ArgumentList $displayArgs & $dotnetInstallPath -InstallDir $InstallRoot -Version $Version -NoPath @verboseParam | ForEach-Object { Write-Information $_ } } else { $cmdName = 'dotnet' $dotnetInstallPath = Join-Path -Path $whiskeyBinPath -ChildPath 'dotnet-install.sh' | Resolve-WhiskeyRelativePath $displayArgs[0] = $dotnetInstallPath Write-WhiskeyCommand -Path 'bash' -ArgumentList $displayArgs bash $dotnetInstallPath -InstallDir $InstallRoot -Version $Version | ForEach-Object { Write-Information $_ } Write-WhiskeyDebug 'Install complete.' } $dotnetPath = Join-Path -Path $InstallRoot -ChildPath $cmdName -Resolve -ErrorAction Ignore if( -not $dotnetPath ) { $msg = "After attempting to install .NET Core SDK version ""$($Version)"", the ""$($cmdName)"" command was " + "not found in ""$($InstallRoot)""." Write-WhiskeyError -Message $msg return } $sdkPath = Join-Path -Path $InstallRoot -ChildPath ('sdk\{0}' -f $Version) -Resolve -ErrorAction Ignore if( -not $sdkPath ) { $msg = "The ""$($cmdName)"" command was installed but .NET SDK ""$($Version)"" doesn't exist in " + """$(Join-Path -Path $InstallRoot -ChildPath 'sdk')""." Write-WhiskeyError -Message $msg return } return $dotnetPath } function Install-WhiskeyDotNetTool { <# .SYNOPSIS Installs the .NET Core SDK tooling for a Whiskey task. .DESCRIPTION The `Install-WhiskeyDotNetTool` function installs the desired version of the .NET Core SDK for a Whiskey task. When given a `Version` the function will attempt to resolve that version to a valid released version of the SDK. If `Version` is null the function will search for a `global.json` file, first in the `WorkingDirectory` and then the `InstallRoot`, and if found it will look for the desired SDK verson in the `sdk.version` property of that file. After installing the SDK the function will update the `global.json`, creating it in the `InstallRoot` if it doesn't exist, `sdk.version` property with the installed version of the SDK. The function returns the path to the installed `dotnet.exe` command. .EXAMPLE Install-WhiskeyDotNetTool -InstallRoot 'C:\Build\Project' -WorkingDirectory 'C:\Build\Project\src' -Version '2.1.4' Demonstrates installing version '2.1.4' of the .NET Core SDK to a '.dotnet' directory in the 'C:\Build\Project' directory. .EXAMPLE Install-WhiskeyDotNetTool -InstallRoot 'C:\Build\Project' -WorkingDirectory 'C:\Build\Project\src' -Version '2.*' Demonstrates installing the latest '2.*' version of the .NET Core SDK to a '.dotnet' directory in the 'C:\Build\Project' directory. .EXAMPLE Install-WhiskeyDotNetTool -InstallRoot 'C:\Build\Project' -WorkingDirectory 'C:\Build\Project\src' Demonstrates installing the version of the .NET Core SDK specified in the `sdk.version` property of the `global.json` file found in either the `WorkingDirectory` or the `InstallRoot` paths. #> [CmdletBinding()] param( [Parameter(Mandatory)] # Path where the `.dotnet` directory will be installed containing the .NET Core SDK. [String] $InstallRoot, [Parameter(Mandatory)] # The working directory of the task requiring the .NET Core SDK tool. This path is used for searching for an # existing `global.json` file containing an SDK version value. [String] $WorkingDirectory, [AllowEmptyString()] [AllowNull()] # The version of the .NET Core SDK to install. Accepts wildcards. [String]$Version ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $WorkingDirectory = Resolve-Path -Path $WorkingDirectory if (-not $WorkingDirectory) { return } Push-Location $WorkingDirectory try { if ($Version) { $sdkVersion = Resolve-WhiskeyDotNetSdkVersion -Version $Version $installRoot = Join-Path -Path $InstallRoot -ChildPath '.dotnet' return Install-WhiskeyDotNetSdk -InstallRoot $installRoot -Version $sdkVersion } if ((Get-Command -Name 'dotnet' -ErrorAction Ignore)) { # This command prints out the version of the .NET SDK that the "dotnet" command would use after taking into # account the global.json file if it exists. Exit code 0 means a compatible SDK was found $possibleVersion = Invoke-Command -ScriptBlock { $ErrorActionPreference = 'Continue' dotnet --version 2>$null } if ( $LASTEXITCODE -eq 0 ) { $msg = "[$($MyInvocation.MyCommand)] Using globally installed .NET SDK version $($possibleVersion)" Write-WhiskeyVerbose -Message $msg return 'dotnet' } } $globalJsonPath = Join-Path -Path $WorkingDirectory -ChildPath 'global.json' if ( -not (Test-Path -Path $globalJsonPath -PathType Leaf) ) { $globalJsonPath = Join-Path -Path $InstallRoot -ChildPath 'global.json' } $sdkVersion = $null if ( Test-Path -Path $globalJsonPath -PathType Leaf ) { try { $globalJson = Get-Content -Path $globalJsonPath -Raw | ConvertFrom-Json } catch { $msg = "Failed to install .NET because global.json file ""$($globalJsonPath)"" contains invalid JSON." Write-WhiskeyError -Message $msg return } $globalJsonSdkOptions = $globalJson | Select-Object -ExpandProperty 'sdk' -ErrorAction Ignore $globalJsonVersion = $globalJsonSdkOptions | Select-Object -ExpandProperty 'version' -ErrorAction Ignore $globalJsonRollForward = $globalJsonSdkOptions | Select-Object -ExpandProperty 'rollForward' -ErrorAction Ignore $rollForwardStrategy = [WhiskeyDotNetSdkRollForward]::Disable $validRollForwardValues = [WhiskeyDotNetSdkRollForward].GetEnumNames() if ($globalJsonRollForward) { if ($globalJsonRollForward -notin $validRollForwardValues) { $msg = "Using default roll forward strategy ""$($rollForwardStrategy)"" because the " + "sdk.rollForward value ""$($globalJsonRollForward)"" in ""$($globalJsonPath)"" is not " + "recognized. Accepted values are: $($validRollForwardValues -join ', ')." + [Environment]::NewLine + [Environment]::NewLine + "If ""$($globalJsonRollForward)"" is a new roll forward strategy, " + '[submit a Whiskey issue](https://github.com/webmd-health-services/Whiskey/issues) ' + 'requesting that we add it.' Write-Warning -Message $msg } else { $rollForwardStrategy = [WhiskeyDotNetSdkRollForward] $globalJsonRollForward } } if ( $globalJsonVersion ) { $msg = "[$($MyInvocation.MyCommand)] .NET Core SDK version '$($globalJsonVersion)' with rollforward value '$($globalJsonRollForward)' found in '$($globalJsonPath)'" Write-WhiskeyVerbose -Message $msg $sdkVersion = Resolve-WhiskeyDotNetSdkVersion -Version $globalJsonVersion -RollForward $rollForwardStrategy } } if ( -not $sdkVersion ) { Write-WhiskeyVerbose -Message ('[{0}] No specific .NET Core SDK version found in whiskey.yml or global.json. Using latest LTS version.' -f $MyInvocation.MyCommand) $sdkVersion = Resolve-WhiskeyDotNetSdkVersion -LatestLTS } $installRoot = Join-Path -Path $InstallRoot -ChildPath '.dotnet' $dotnetPath = Install-WhiskeyDotNetSdk -InstallRoot $installRoot -Version $sdkVersion return $dotnetPath } finally { Pop-Location } } function Install-WhiskeyNode { [CmdletBinding()] param( # The directory where Node should be installed. Will actually be installed into # `Join-Path -Path $InstallRootPath -ChildPath '.node'`. [Parameter(Mandatory)] [String] $InstallRootPath, # The directory where the Node.js package file should be downloaded. [Parameter(Mandatory)] [String] $OutFileRootPath, # Are we running in clean mode? If so, don't re-install the tool. [switch] $InCleanMode, # The version of Node to install. If not provided, will use the version defined in the package.json file. If # that isn't supplied, will install the latest LTS version. [String] $Version ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $InstallRootPath -ErrorAction Ignore if( $InCleanMode ) { if( $nodePath ) { return $nodePath } return } $npmVersionToInstall = $null $nodeVersionToInstall = $null $ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue $nodeVersions = Invoke-RestMethod -Uri 'https://nodejs.org/dist/index.json' | ForEach-Object { $_ } if( $Version ) { $nodeVersionToInstall = $nodeVersions | Where-Object { $_.version -like 'v{0}' -f $Version } | Select-Object -First 1 if( -not $nodeVersionToInstall ) { throw ('Node v{0} does not exist.' -f $Version) } } else { $packageJsonPath = Join-Path -Path (Get-Location).ProviderPath -ChildPath 'package.json' if( -not (Test-Path -Path $packageJsonPath -PathType Leaf) ) { $packageJsonPath = Join-Path -Path $InstallRootPath -ChildPath 'package.json' } if( (Test-Path -Path $packageJsonPath -PathType Leaf) ) { Write-WhiskeyVerbose -Message ('Reading ''{0}'' to determine Node and NPM versions to use.' -f $packageJsonPath) $packageJson = Get-Content -Raw -Path $packageJsonPath | ConvertFrom-Json if( $packageJson -and ($packageJson | Get-Member 'engines') ) { if( ($packageJson.engines | Get-Member 'node') -and $packageJson.engines.node -match '(\d+\.\d+\.\d+)' ) { $nodeVersionToInstall = 'v{0}' -f $Matches[1] $nodeVersionToInstall = $nodeVersions | Where-Object { $_.version -eq $nodeVersionToInstall } | Select-Object -First 1 } if( ($packageJson.engines | Get-Member 'npm') -and $packageJson.engines.npm -match '(\d+\.\d+\.\d+)' ) { $npmVersionToInstall = $Matches[1] } } } } if( -not $nodeVersionToInstall ) { $nodeVersionToInstall = $nodeVersions | Where-Object { ($_ | Get-Member 'lts') -and $_.lts } | Select-Object -First 1 } if( -not $npmVersionToInstall ) { $npmVersionToInstall = $nodeVersionToInstall.npm } $installNode = $false if( $nodePath ) { $currentNodeVersion = & $nodePath '--version' if( $currentNodeVersion -ne $nodeVersionToInstall.version ) { Uninstall-WhiskeyNode -InstallRoot $InstallRootPath $installNode = $true } } else { $installNode = $true } $nodeDirectoryName = '.node' $nodeRoot = Join-Path -Path $InstallRootPath -ChildPath $nodeDirectoryName $platform = 'win' $packageExtension = 'zip' if( $IsLinux ) { $platform = 'linux' $packageExtension = 'tar.xz' } elseif( $IsMacOS ) { $platform = 'darwin' $packageExtension = 'tar.gz' } $extractedDirName = 'node-{0}-{1}-x64' -f $nodeVersionToInstall.version,$platform $filename = '{0}.{1}' -f $extractedDirName,$packageExtension if( $installNode ) { $nodeZipFilePath = Join-Path -Path $OutFileRootPath -ChildPath $filename if( -not (Test-Path -Path $nodeZipFilePath -PathType Leaf) ) { $uri = 'https://nodejs.org/dist/{0}/{1}' -f $nodeVersionToInstall.version,$filename if( -not (Test-Path -Path $OutFileRootPath) ) { Write-WhiskeyDebug -Message "Creating output directory ""$($OutFileRootPath)""." New-Item -Path $OutFileRootPath -ItemType 'Directory' -Force | Out-Null } $preExistingPkgPath = Join-Path -Path $OutFileRootPath -ChildPath "node-*-*-x64.$($packageExtension)" if( (Test-Path -Path $preExistingPkgPath) ) { Remove-Item -Path $preExistingPkgPath -Force -ErrorAction Ignore } try { $ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue Invoke-WebRequest -Uri $uri -OutFile $nodeZipFilePath } catch { $responseInfo = '' $notFound = $false if( $_.Exception | Get-Member -Name 'Response' ) { $responseStatus = $_.Exception.Response.StatusCode $responseInfo = ' Received a {0} ({1}) response.' -f $responseStatus,[int]$responseStatus if( $responseStatus -eq [Net.HttpStatusCode]::NotFound ) { $notFound = $true } } else { Write-WhiskeyError -Message "Exception downloading ""$($uri)"": $($_)" $responseInfo = ' Please see previous error for more information.' } $errorMsg = "Failed to download Node $($nodeVersionToInstall.version) from $($uri).$($responseInfo)" if( $notFound ) { $errorMsg = "$($errorMsg) It looks like this version of Node wasn't packaged as a ZIP file. " + 'Please use Node v4.5.0 or newer.' } Write-WhiskeyError -Message $errorMsg -ErrorAction Stop return } } if( $IsWindows ) { # Windows/.NET can't handle the long paths in the Node package, so on that platform, we need to download # 7-zip because it can handle long paths. $7zipPackageRoot = Install-WhiskeyTool -Name '7-Zip.CommandLine' ` -ProviderName 'NuGet' ` -Version '18.*' ` -InstallRoot $InstallRootPath $7z = Join-Path -Path $7zipPackageRoot -ChildPath 'tools\x64\7za.exe' -Resolve -ErrorAction Stop $archive = [IO.Compression.ZipFile]::OpenRead($nodeZipFilePath) $outputDirectoryName = $archive.Entries[0].FullName $archive.Dispose() $outputDirectoryName = $outputDirectoryName.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) $outputRoot = Join-Path -Path $InstallRootPath -ChildPath $outputDirectoryName Write-WhiskeyVerbose -Message ('{0} x {1} -o{2} -y' -f $7z,$nodeZipFilePath,$outputRoot) & $7z -spe 'x' $nodeZipFilePath ('-o{0}' -f $outputRoot) '-y' | Write-WhiskeyVerbose # We use New-TimeSpan so we can mock it and wait for our simulated anti-virus process to lock a # file (i.e. so we can test that this wait logic works). $maxTime = New-TimeSpan -Seconds 10 $timer = [Diagnostics.Stopwatch]::StartNew() $exists = $false $lastError = $null Write-WhiskeyDebug "Renaming ""$($outputRoot)"" -> ""$($nodeDirectoryName)""." do { Rename-Item -Path $outputRoot -NewName $nodeDirectoryName -ErrorAction SilentlyContinue $exists = Test-Path -Path $nodeRoot -PathType Container if( $exists ) { Write-WhiskeyDebug "Rename succeeded." break } $lastError = $Global:Error | Select-Object -First 1 Write-WhiskeyDebug -Message "Rename failed: $($lastError)" $Global:Error.RemoveAt(0) Start-Sleep -Seconds 1 } while( $timer.Elapsed -lt $maxTime ) if( -not $exists ) { $msg = "Failed to install Node to ""$($nodeRoot)"" because renaming directory " + """$($outputDirectoryName)"" to ""$($nodeDirectoryName)"" failed: $($lastError)" Write-WhiskeyError -Message $msg } } else { if( -not (Test-Path -Path $nodeRoot -PathType Container) ) { New-Item -Path $nodeRoot -ItemType 'Directory' -Force | Out-Null } Write-WhiskeyVerbose -Message ('tar -xJf "{0}" -C "{1}" --strip-components=1' -f $nodeZipFilePath,$nodeRoot) tar -xJf $nodeZipFilePath -C $nodeRoot '--strip-components=1' | Write-WhiskeyVerbose if( $LASTEXITCODE ) { Write-WhiskeyError -Message ('Failed to extract Node.js {0} package "{1}" to "{2}".' -f $nodeVersionToInstall.version,$nodeZipFilePath,$nodeRoot) return } } $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $InstallRootPath -ErrorAction Stop } $npmPath = Resolve-WhiskeyNodeModulePath -Name 'npm' -NodeRootPath $nodeRoot -ErrorAction Stop $npmPath = Join-Path -Path $npmPath -ChildPath 'bin\npm-cli.js' $npmVersion = & $nodePath $npmPath '--version' if( $npmVersion -ne $npmVersionToInstall ) { Write-WhiskeyInfo ('Installing npm@{0}.' -f $npmVersionToInstall) # Bug in NPM 5 that won't delete these files in the node home directory. Get-ChildItem -Path (Join-Path -Path $nodeRoot -ChildPath '*') -Include 'npm.cmd','npm','npx.cmd','npx' | Remove-Item & $nodePath $npmPath 'install' ('npm@{0}' -f $npmVersionToInstall) '-g' if( $LASTEXITCODE ) { "Failed to update to NPM $($npmVersionToInstall). See previous output for details." | Write-WhiskeyError } } return $nodePath } function Install-WhiskeyNodeModule { <# .SYNOPSIS Installs Node.js modules .DESCRIPTION The `Install-WhiskeyNodeModule` function installs Node.js modules to the `node_modules` directory located in the current working directory. The path to the module's directory is returned. Failing to install a module does not cause a bulid to fail. If you want a build to fail if the module fails to install, you must pass `-ErrorAction Stop`. .EXAMPLE Install-WhiskeyNodeModule -Name 'rimraf' -Version '^2.0.0' -NodePath $TaskParameter['NodePath'] This example will install the Node module `rimraf` at the latest `2.x.x` version in the `node_modules` directory located in the current directory. .EXAMPLE Install-WhiskeyNodeModule -Name 'rimraf' -Version '^2.0.0' -NodePath $TaskParameter['NodePath -ErrorAction Stop Demonstrates how to fail a build if installing the module fails by setting the `ErrorAction` parameter to `Stop`. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The name of the module to install. [String]$Name, # The version of the module to install. [String]$Version, # Node modules are being installed on a developer computer. [switch]$ForDeveloper, [Parameter(Mandatory)] # The path to the build root. [String]$BuildRootPath, # Whether or not to install the module globally. [switch]$Global, # Are we running in clean mode? [switch]$InCleanMode ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $npmArgument = & { if( $Version ) { ('{0}@{1}' -f $Name,$Version) } else { $Name } if( $Global ) { '-g' } } $modulePath = Resolve-WhiskeyNodeModulePath -Name $Name -BuildRootPath $BuildRootPath -Global:$Global -ErrorAction Ignore if( $modulePath ) { return $modulePath } elseif( $InCleanMode ) { return } Invoke-WhiskeyNpmCommand -Name 'install' -ArgumentList $npmArgument -BuildRootPath $BuildRootPath -ForDeveloper:$ForDeveloper | Write-WhiskeyVerbose if( $LASTEXITCODE ) { return } $modulePath = Resolve-WhiskeyNodeModulePath -Name $Name -BuildRootPath $BuildRootPath -Global:$Global -ErrorAction Ignore if( -not $modulePath ) { Write-WhiskeyError -Message ('NPM executed successfully when attempting to install "{0}" but the module was not found anywhere in the build root "{1}"' -f ($npmArgument -join ' '),$BuildRootPath) return } return $modulePath } function Install-WhiskeyNuGetPackage { [CmdletBinding()] param( [Parameter(Mandatory)] [String] $Name, [String] $Version, [Parameter(Mandatory)] [String] $BuildRootPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState # Sometimes finding packages can be flaky, so we try multiple times. $numErrors = $Global:Error.Count $numTries = 6 $waitMilliseconds = 100 $allPkgs = @() $pkgMgmtErrors = @{} for( $idx = 0; $idx -lt $numTries; ++$idx ) { $allPkgs = Find-Package -Name $Name ` -ProviderName 'NuGet' ` -AllVersions ` -ErrorAction SilentlyContinue ` -ErrorVariable 'pkgMgmtErrors' | Where-Object { $_ -notmatch '-' } if( $allPkgs ) { Write-WhiskeyVerbose "Found $(($allPkgs | Measure-Object).Count) $($Name) packages:" foreach ($pkg in $allPkgs) { Write-WhiskeyVerbose " $($pkg.Name) $($pkg.Version) from $($pkg.Source) [$($pkg.GetType().FullName)]" } break } Start-Sleep -Milliseconds $waitMilliseconds $waitMilliseconds = $waitMilliseconds + 2 } if (-not $allPkgs) { $pkgMgmtErrors | Write-Error $msg = "NuGet package $($Name) $($Version) does not exist or search request failed." Write-WhiskeyError -Message $msg return } for ($idx = 0; $idx -lt $Global:Error.Count - $numErrors; ++$idx) { $Global:Error.RemoveAt(0) } $pkg = $allPkgs | Select-Object -First 1 if ($Version) { $pkgVersions = $allPkgs | Where-Object 'Version' -Like $Version if (-not $pkgVersions) { # Some package versions have build metadata at the end. $pkgVersions = $allPkgs | Where-Object 'Version' -Like "$($Version)+*" } if (-not $pkgVersions) { "Failed to install NuGet package $($Name) $($Version) because that version does not exist. Available " + 'versions are:' + [Environment]::NewLine + [Environment]::NewLine + "* $(($allPkgs | Select-Object -ExpandProperty 'Version') -join "$([Environment]::NewLine)* ")" | Write-WhiskeyError return } Write-WhiskeyVerbose "Found $(($pkgVersions | Measure-Object).Count) $($Name) $($Version) packages:" foreach ($pkg in $pkgVersions) { Write-WhiskeyVerbose " $($pkg.Name) $($pkg.Version) from $($pkg.Source) [$($pkg.GetType().FullName)]" } $pkg = $pkgVersions | Select-Object -First 1 } "Found package $($pkg.Name) $($pkg.Version) from $($pkg.Source)." | Write-WhiskeyVerbose $pkgBaseName = "$($Name).$($pkg.Version -replace '\+.*$', '')" # Save-Module downloads dependencies, too. Save everything for a package into its own directory so we know which # packages to install as dependencies. $cachePath = Join-Path -Path $BuildRootPath -ChildPath ".output\nuget\$($pkgBaseName)" if( -not (Test-Path -Path $cachePath) ) { New-Item -Path $cachePath -ItemType 'Directory' | Out-Null } $nupkgPath = Join-Path -Path $cachePath -ChildPath "$($pkgBaseName).nupkg" $packagesPath = Join-Path -Path $BuildRootPath -ChildPath 'packages' if (-not (Test-Path -Path $packagesPath)) { New-Item -Path $packagesPath -ItemType 'Directory' | Out-Null } if( -not (Test-Path -Path $nupkgPath) ) { $waitMilliseconds = 100 $numErrors = $Global:Error.Count $pkgMgmtErrors = @() for( $idx = 0; $idx -lt $numTries; ++$idx ) { $destinationPath = [IO.Path]::GetFileNameWithoutExtension($nupkgPath) $destinationPath = Join-Path -Path ($packagesPath | Resolve-Path -Relative) -ChildPath $destinationPath "Saving NuGet package $($pkg.Name) $($pkg.Version) to ""$($destinationPath)""." | Write-WhiskeyInfo $pkg | Save-Package -Path $cachePath -ErrorAction SilentlyContinue -ErrorVariable 'pkgMgmtErrors' -Force | Out-Null if( (Test-Path -Path $nupkgPath) ) { break } Start-Sleep -Milliseconds $waitMilliseconds $waitMilliseconds = $waitMilliseconds * 2 } if( -not (Test-Path -Path $nupkgPath) ) { $pkgMgmtErrors | Write-Error $msg = "Failed to download NuGet package $($pkg.Name) $($pkg.Version)." Write-WhiskeyError -Message $msg return } for( $idx = 0; $idx -lt $Global:Error.Count - $numErrors; ++$idx ) { $Global:Error.RemoveAt(0) } } # Install the package and all its dependencies into 'packages'. foreach( $pkgInfo in (Get-ChildItem -Path $cachePath -Filter '*.nupkg') ) { $pkgPath = Join-Path -Path $packagesPath -ChildPath $pkgInfo.BaseName if( -not (Test-Path -Path $pkgPath) ) { New-Item -Path $pkgPath -ItemType 'Directory' -Force | Out-Null } if( -not (Get-ChildItem -LiteralPath $pkgPath) ) { Write-WhiskeyVerbose -Message "Extracting ""$($pkgInfo.Name)"" to ""$($pkgPath | Resolve-Path -Relative)""." Add-Type -AssemblyName 'System.IO.Compression.FileSystem' [IO.Compression.ZipFile]::ExtractToDirectory($pkgInfo.FullName, $pkgPath) } } return Join-Path -Path $packagesPath -ChildPath $pkgBaseName } function Install-WhiskeyPowerShellModule { <# .SYNOPSIS Installs a PowerShell module. .DESCRIPTION The `Install-WhiskeyPowerShellModule` function installs a PowerShell module into a "PSModules" directory in the current working directory. It returns the path to the module. .EXAMPLE Install-WhiskeyPowerShellModule -Name 'Pester' -Version '4.3.0' This example will install the PowerShell module `Pester` at version `4.3.0` version in the `PSModules` directory. .EXAMPLE Install-WhiskeyPowerShellModule -Name 'Pester' -Version '4.*' Demonstrates that you can use wildcards to choose the latest minor version of a module. .EXAMPLE Install-WhiskeyPowerShellModule -Name 'Pester' -Version '4.3.0' -ErrorAction Stop Demonstrates how to fail a build if installing the module fails by setting the `ErrorAction` parameter to `Stop`. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The name of the module to install. [String]$Name, # The version of the module to install. [String]$Version, [Parameter(Mandatory)] # Modules are saved into a PSModules directory. This is the directory where PSModules directory should created, *not* the path to the PSModules directory itself, i.e. this is the path to the "PSModules" directory's parent directory. [String]$BuildRoot, # The path to a custom directory where you want the module installed. The default is `PSModules` in the build root. [String]$Path, # Don't import the module. [switch]$SkipImport, # Allow prerelease versions. [switch]$AllowPrerelease ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-WhiskeyDebug "\Install-WhiskeyPowerShellModule\" -Indent try { function Find-PSModule { $findParameters = @{ 'Name' = $Name; 'BuildRoot' = $BuildRoot; 'AllowPrerelease' = $AllowPrerelease; 'Version' = $Version; } return Find-WhiskeyPowerShellModule @findParameters } Write-WhiskeyDebug ($PSBoundParameters | Format-Table | Out-String) if( $Path ) { $Path = $Path.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) if( -not [IO.Path]::IsPathRooted($Path) ) { $Path = Join-Path -Path $BuildRoot -ChildPath $Path $Path = [IO.Path]::GetFullPath($Path) } if( -not (Test-Path -Path $Path) ) { New-Item -Path $Path -ItemType 'Directory' | Out-Null } # Whiskey's PowerShell functions assume all modules are installed in a path in PSModulePath environment variable # so make sure the user's path is in that path. Register-WhiskeyPSModulePath -Path $Path $installRoot = $Path } else { $installRoot = Get-WhiskeyPSModulePath -PSModulesRoot $BuildRoot -Create } Write-WhiskeyDebug "Module $($Name) $($Version) will be installed to ""$($installRoot)""." if( -not $Version ) { Write-WhiskeyDebug "Searching for latest $($Name) module version." # We need to know the latest version of the module so we can see if it is already installed. $latestModule = Find-PSModule if( -not $latestModule ) { Write-WhiskeyDebug "Module $($Name) not found in any repository." return } Write-WhiskeyDebug ($latestModule | Format-List | Out-String) $Version = $latestModule.Version } try { $installedModule = Get-WhiskeyPSModule -PSModulesRoot $BuildRoot -Name $Name -Version $Version if( $installedModule ) { Write-WhiskeyDebug "Module $($Name) $($Version) found:$([Environment]::NewLine)$($installedModule | Format-List)" $installedInPSModulePath = -not $Path $installedInCustomPath = $Path -and ` ($installedModule.Path | Split-Path | Split-Path | Split-Path) -eq $Path if( $installedInPSModulePath -or $installedInCustomPath ) { if( -not $SkipImport ) { Import-WhiskeyPowerShellModule -Name $Name -Version $installedModule.Version -PSModulesRoot $BuildRoot } # Already installed or installed where the user wants it. return $installedModule } } Write-WhiskeyDebug "Module $($Name) $($Version) not installed." # Find what module *should* be installed. $moduleToInstall = Find-PSModule if( -not $moduleToInstall ) { Write-WhiskeyDebug "Module $($Name) $($Version) not found in any repository." return } Write-WhiskeyDebug "Module $($Name) $($Version) found: $($moduleToInstall | Format-List | Out-String)." # Now we know where the module is going to be saved, let's make sure the destination doesn't exist. $moduleRoot = Join-Path -Path $installRoot -ChildPath $moduleToInstall.Name $moduleRoot = Join-Path -Path $moduleRoot -ChildPath $moduleToInstall.Version if( (Test-Path -Path $moduleRoot) ) { Write-WhiskeyDebug "Removing module $($Name) $($Version) destination directory ""$($moduleRoot)""." Remove-Item -Path $moduleRoot -Recurse -Force if( (Test-Path -Path $moduleRoot) ) { $msg = "Unable to install PowerShell module $($moduleToInstall.Name) $($moduleToInstall.Version) to " + """$($installRoot | Resolve-Path -Relative)"": the destination path " + """$($moduleRoot | Resolve-Path -Relative)"" exists and deleting it failed. Make sure files " + 'under the destination directory aren''t in use.' Write-WhiskeyError -Message $msg return } } $msg = "Saving PowerShell module ""$($moduleToInstall.Name)"" $($moduleToInstall.Version) from repository " + """$($moduleToInstall.Repository)"" to ""$($installRoot)""." Write-WhiskeyVerbose -Message $msg $globalProgressPref = $Global:ProgressPreference # Ignore doesn't work in Windows PowerShell. $Global:ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue $allowPrereleaseArg = Get-AllowPrereleaseArg -CommandName 'Save-Module' -AllowPrerelease:$AllowPrerelease try { Save-Module -Name $moduleToInstall.Name ` -RequiredVersion $moduleToInstall.Version ` -Repository $moduleToInstall.Repository ` -Path $installRoot ` @allowPrereleaseArg } finally { $Global:ProgressPreference = $globalProgressPref } $installedModule = Get-WhiskeyPSModule -PSModulesRoot $BuildRoot ` -Name $moduleToInstall.Name ` -Version $moduleToInstall.Version if( -not $installedModule ) { $msg = "Failed to download PowerShell module $($moduleToInstall.Name) $($moduleToInstall.Version) from " + "repository $($moduleToInstall.Repository) to ""$($installRoot | Resolve-Path -Relative)"": the " + 'module doesn''t exist after running PowerShell''s "Save-Module" command.' Write-WhiskeyError -Message $msg return } $installedModule | Write-Output if( -not $SkipImport ) { Import-WhiskeyPowerShellModule -Name $Name -Version $installedModule.Version -PSModulesRoot $BuildRoot } } finally { if( $Path ) { # Remove the user's path from the PSModulePath environment variable. Unregister-WhiskeyPSModulePath -Path $Path } } } finally { Write-WhiskeyDebug "/Install-WhiskeyPowerShellModule/" -Outdent } } function Install-WhiskeyTool { <# .SYNOPSIS Downloads and installs tools needed by the Whiskey module. .DESCRIPTION The `Install-WhiskeyTool` function downloads and installs PowerShell modules or NuGet Packages needed by functions in the Whiskey module. PowerShell modules are installed to a `Modules` directory in your build root. If PowerShell modules are already installed globally and are listed in the PSModulePath environment variable, they will not be re-installed. A `DirectoryInfo` object for the downloaded tool's directory is returned. `Install-WhiskeyTool` also installs tools that are needed by tasks. Tasks define the tools they need with a [Whiskey.RequiresTool()] attribute in the tasks function. Supported tools are 'Node', 'NodeModule', and 'DotNet'. Users of the `Whiskey` API typcially won't need to use this function. It is called by other `Whiskey` function so they have the tools they need. .EXAMPLE Install-WhiskeyTool -NugetPackageName 'NUnit.Runners' -version '2.6.4' Demonstrates how to install a specific version of a NuGet Package. In this case, NUnit Runners version 2.6.4 would be installed. #> [CmdletBinding()] param( # The attribute that defines what tool is necessary. [Parameter(Mandatory, ParameterSetName='FromAttribute')] [Whiskey.RequiresToolAttribute] $ToolInfo, # The task parameters for the currently running task. [Parameter(Mandatory, ParameterSetName='FromAttribute')] [hashtable] $TaskParameter, # Running in clean mode, so don't install the tool if it isn't installed. [Parameter(ParameterSetName='FromAttribute')] [switch] $InCleanMode, # The path to a directory where downloaded package files should be saved prior to installation. [Parameter(Mandatory, ParameterSetName='FromAttribute')] [Parameter(ParameterSetName='AtRuntime')] [String] $OutFileRootPath, [Parameter(ParameterSetName='AtRuntime')] [AllowEmptyString()] [String] $ProviderName, [Parameter(Mandatory, ParameterSetName='AtRuntime')] [String] $Name, # The name of the NuGet package to download. [Parameter(Mandatory, ParameterSetName='NuGet')] [String] $NuGetPackageName, [Parameter(ParameterSetName='NuGet')] [Parameter(ParameterSetName='AtRuntime')] [String] $Version, # The directory where you want the tools installed. [Parameter(Mandatory, ParameterSetName='FromAttribute')] [Parameter(Mandatory, ParameterSetName='AtRuntime')] [String] $InstallRoot, # The root directory where the tools should be downloaded. The default is your build root. # # PowerShell modules are saved to `$DownloadRoot\Modules`. # # NuGet packages are saved to `$DownloadRoot\packages`. [Parameter(Mandatory, ParameterSetName='NuGet')] [String] $DownloadRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-WhiskeyDebug '\Install-WhiskeyTool\' -Indent try { $mutexName = $InstallRoot if( $DownloadRoot ) { $mutexName = $DownloadRoot } # Back slashes in mutex names are reserved. $mutexName = $mutexName -replace '\\','/' $mutexName = $mutexName -replace '/','-' $startedWaitingAt = Get-Date $startedUsingAt = Get-Date Write-WhiskeyDebug -Message ('Creating mutex "{0}".' -f $mutexName) $installLock = New-Object 'Threading.Mutex' $false,$mutexName #$DebugPreference = 'Continue' Write-WhiskeyDebug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" is waiting for mutex "{2}".' -f (Get-Date),$PID,$mutexName) try { try { [Void]$installLock.WaitOne() } catch [Threading.AbandonedMutexException] { Write-WhiskeyDebug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" caught "{2}" exception waiting to acquire mutex "{3}": {4}.' -f (Get-Date),$PID,$_.Exception.GetType().FullName,$mutexName,$_) $Global:Error.RemoveAt(0) } $waitedFor = (Get-Date) - $startedWaitingAt Write-WhiskeyDebug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" obtained mutex "{2}" in {3}.' -f (Get-Date),$PID,$mutexName,$waitedFor) #$DebugPreference = 'SilentlyContinue' $startedUsingAt = Get-Date if( $PSCmdlet.ParameterSetName -eq 'NuGet' ) { $msg = 'The "Install-WhiskeyTool" function''s "NuGetPackage" name parameter is obsolete. Use ' + '[Whiskey.WhiskeyTool] attribute on your task instead.' Write-Warning -Message $msg Install-WhiskeyNuGetPackage -Name $NuGetPackageName -Version $Version -BuildRootPath $DownloadRoot return } if( $PSCmdlet.ParameterSetName -eq 'FromAttribute' ) { $ProviderName = $ToolInfo.ProviderName $Name = $ToolInfo.Name $Version = $TaskParameter[$ToolInfo.VersionParameterName] if( -not $Version ) { $Version = $ToolInfo.Version } } if( -not $OutFileRootPath ) { $OutFileRootPath = Join-Path -Path $InstallRoot -ChildPath '.output' } if( -not (Test-Path -Path $OutFileRootPath) ) { New-Item -Path $OutFileRootPath -ItemType 'Directory' | Out-Null } if( $ToolInfo -is [Whiskey.RequiresPowerShellModuleAttribute] ) { $module = Install-WhiskeyPowerShellModule -Name $Name ` -Version $Version ` -BuildRoot $InstallRoot ` -SkipImport:$ToolInfo.SkipImport ` -ErrorAction Stop if( $ToolInfo.ModuleInfoParameterName ) { $TaskParameter[$ToolInfo.ModuleInfoParameterName] = $module } return } $toolPath = $null switch( $ProviderName ) { 'NuGet' { $toolPath = Install-WhiskeyNuGetPackage -Name $Name -Version $Version -BuildRootPath $InstallRoot } 'NodeModule' { $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $InstallRoot if( -not $nodePath ) { $msg = 'It looks like Node isn''t installed in your repository. Whiskey usually installs Node.js ' + 'for you into a .node directory. If this directory doesn''t exist, this is most likely a ' + 'task authoring error and the author of your task needs to add a `WhiskeyTool` attribute ' + 'declaring it has a dependency on Node.js. If the .node directory exists, the Node ' + 'package is most likely corrupt. Please delete it and re-run your build.' Write-WhiskeyError -Message $msg -ErrorAction Stop return } $toolPath = Install-WhiskeyNodeModule -Name $Name ` -BuildRootPath $InstallRoot ` -Version $Version ` -Global ` -InCleanMode:$InCleanMode ` -ErrorAction Stop } default { switch( $Name ) { 'Node' { $toolPath = Install-WhiskeyNode -InstallRootPath $InstallRoot ` -Version $Version ` -InCleanMode:$InCleanMode ` -OutFileRootPath $OutFileRootPath } 'DotNet' { $toolPath = Install-WhiskeyDotNetTool -InstallRoot $InstallRoot ` -WorkingDirectory (Get-Location).ProviderPath ` -Version $Version ` -ErrorAction Stop } default { throw "Unknown tool ""$($Name)"". The only supported tools are ""Node"" and ""DotNet""." } } } } if( $PSCmdlet.ParameterSetName -eq 'FromAttribute' -and $ToolInfo.PathParameterName ) { $TaskParameter[$ToolInfo.PathParameterName] = $toolPath } return $toolPath } finally { #$DebugPreference = 'Continue' $usedFor = (Get-Date) - $startedUsingAt Write-WhiskeyDebug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" releasing mutex "{2}" after using it for {3}.' -f (Get-Date),$PID,$mutexName,$usedFor) $startedReleasingAt = Get-Date $installLock.ReleaseMutex(); $installLock.Dispose() $installLock.Close() $installLock = $null $releasedDuration = (Get-Date) - $startedReleasingAt Write-WhiskeyDebug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" released mutex "{2}" in {3}.' -f (Get-Date),$PID,$mutexName,$releasedDuration) #$DebugPreference = 'SilentlyContinue' } } finally { Write-WhiskeyDebug '/Install-WhiskeyTool/' -Outdent } } function Invoke-WhiskeyBuild { <# .SYNOPSIS Runs a build. .DESCRIPTION The `Invoke-WhiskeyBuild` function runs a build as defined by your `whiskey.yml` file. Use the `New-WhiskeyContext` function to create a context object, then pass that context object to `Invoke-WhiskeyBuild`. `New-WhiskeyContext` takes the path to the `whiskey.yml` file you want to run: $context = New-WhiskeyContext -Environment 'Developer' -ConfigurationPath 'whiskey.yml' Invoke-WhiskeyBuild -Context $context Builds can run in three modes: `Build`, `Clean`, and `Initialize`. The default behavior is `Build` mode. In `Build` mode, each task in the `Build` pipeline is run. If you're on a publishing branch, and being run on a build server, each task in the `Publish` pipeline is also run. In `Clean` mode, each task that supports clean mode is run. In this mode, tasks clean up any build artifacts they create. Tasks opt-in to this mode. If a task isn't cleaning up, it should be updated to support clean mode. In `Initialize` mode, each task that suppors initialize mode is run. In this mode, tasks download, install, and configure any tools or other dependencies needed. This mode is intended to be used by developers so they can get any tools needed to start developing without having to run an entire build, which may take a long time. Tasks opt-in to this mode. If a task uses an external tool or dependences, and they don't exist after running in `Initialize` mode, it should be updated to support `Initialize` mode. (Task authors should see the `about_Whiskey_Writing_Tasks` for information about how to opt-in to `Clean` and `Initialize` mode.) Your `whiskey.yml` file can contain multiple pipelines (see `about_Whiskey.yml` for information about `whiskey.yml syntax). Usually, there is a pipeline for each application you want to build. To build specific pipelines, pass the pipeline names to the `PipelineName` parameter. Just those pipeline will be run. The `Publish` pipeline will *not* run unless it is one of the names you pass to the `PipelineName` parameter. .LINK about_Whiskey.yml .LINK New-WhiskeyContext .LINK about_Whiskey_Writing_Tasks .EXAMPLE Invoke-WhiskeyBuild -Context $context Demonstrates how to run a complete build. In this example, the `Build` pipeline is run, and, if running on a build server and on a publishing branch, the `Publish` pipeline is run. .EXAMPLE Invoke-WhiskeyBuild -Context $context -Clean Demonstrates how to run a build in `Clean` mode. In this example, each task in the `Build` and `Publish` pipelines that support `Clean` mode is run so they can delete any build output, downloaded depedencies, etc. .EXAMPLE Invoke-WhiskeyBuild -Context $context -Initialize Demonstrates how to run a build in `Initialize` mode. In this example, each task in the `Build` and `Publish` pipelines that supports `Initialize` mode is run so they can download/install/configure any tools or dependencies. .EXAMPLE Invoke-WhiskeyBuild -Context $context -PipelineName 'App1','App2' Demonstrates how to run specific pipelines. In this example, all the tasks in the `App1` and `App2` pipelines are run. See `about_Whiskey.yml` for information about how to define pipelines. #> [CmdletBinding(DefaultParameterSetName='Build')] param( [Parameter(Mandatory)] # The context for the build. Use `New-WhiskeyContext` to create context objects. [Whiskey.Context]$Context, # The name(s) of any pipelines to run. Default behavior is to run the `Build` pipeline and, if on a publishing # branch, the `Publish` pipeline. # # If you pass a value to this parameter, the `Publish` pipeline is *not* run implicitly. You must pass its name # to run it. [String[]]$PipelineName, [Parameter(Mandatory,ParameterSetName='Clean')] # Runs the build in clean mode. In clean mode, tasks delete any artifacts they create, including downloaded # tools and dependencies. This is opt-in, so if a task is not deleting its artifacts, it needs to be updated to # support clean mode. [switch]$Clean, [Parameter(Mandatory,ParameterSetName='Initialize')] # Runs the build in initialize mode. In initialize mode, tasks download/install/configure any tools/dependencies # they use/need during the build. Initialize mode is intended to be used by developers so that any # tools/dependencies they need can be downloaded/installe/configured without needing to run an entire build, # which can sometimes take a long time. [switch]$Initialize ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $PSBoundParameters.ContainsKey('InformationAction') ) { # Whiskey logs to the information stream so make sure it is enabled. Unless the user wants it off. $InformationPreference = 'Continue' } $Context.StartBuild() Register-WhiskeyPSModulePath -PSModulesRoot $Context.BuildRoot Set-WhiskeyBuildStatus -Context $Context -Status Started $succeeded = $false Push-Location -Path $Context.BuildRoot try { $Context.RunMode = $PSCmdlet.ParameterSetName if( $PipelineName ) { foreach( $name in $PipelineName ) { Invoke-WhiskeyPipeline -Context $Context -Name $name } } else { $config = $Context.Configuration $buildPipelineName = 'Build' if( $config.ContainsKey('BuildTasks') ) { $buildPipelineName = 'BuildTasks' } Invoke-WhiskeyPipeline -Context $Context -Name $buildPipelineName $publishPipelineName = 'Publish' if( $config.ContainsKey('PublishTasks') ) { $publishPipelineName = 'PublishTasks' } Write-WhiskeyVerbose -Context $Context -Message ('Publish? {0}' -f $Context.Publish) Write-WhiskeyVerbose -Context $Context -Message ('Publish Pipeline? {0}' -f $config.ContainsKey($publishPipelineName)) if( $Context.Publish -and $config.ContainsKey($publishPipelineName) ) { Invoke-WhiskeyPipeline -Context $Context -Name $publishPipelineName } } $succeeded = $true } finally { if( $Clean ) { Remove-Item -path $Context.OutputDirectory -Recurse -Force | Out-String | Write-WhiskeyVerbose -Context $Context } Pop-Location $status = 'Failed' if( $succeeded ) { $status = 'Completed' } Set-WhiskeyBuildStatus -Context $Context -Status $status Unregister-WhiskeyPSModulePath -PSModulesRoot $Context.BuildRoot $context.StopBuild() $msg = "$($status) in $(($context.BuildStopwatch | Format-Stopwatch).Trim())" Write-WhiskeyInfo -Context $context -Message $msg -NoTiming -NoIndent } # There are some errors (strict mode validation failures, command not found errors, etc.) that stop a build, but # even though ErrorActionPreference is Stop, it doesn't stop the current process, which is what causes a build to # fail the build. If we get here, and the build didn't succeed, we've encountered one of those errors. Throw a # guaranteed terminating error. if( -not $succeeded ) { Write-Error -Message ('Build failed. See previous error output for more information.') -ErrorAction Stop } } function Invoke-WhiskeyDotNetCommand { <# .SYNOPSIS Runs `dotnet.exe` with a given SDK command and arguments. .DESCRIPTION The `Invoke-WhiskeyDotNetCommand` function runs the `dotnet.exe` executable with a given SDK command and any optional arguments. Pass the path to the `dotnet.exe` to the `DotNetPath` parameter. Pass the name of the SDK command to the `Name` parameter. You may pass a list of arguments to the `dotnet.exe` command with the `ArgumentList` parameter. By default, the `dotnet.exe` command runs with any solution or .csproj files found in the current directory. To run the `dotnet.exe` command with a specific solution or .csproj file pass the path to that file to the `ProjectPath` parameter. .EXAMPLE Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath 'C:\Program Files\dotnet\dotnet.exe' -Name 'build' -ArgumentList '--verbosity minimal','--no-incremental' -ProjectPath 'C:\Build\DotNetCore.csproj' Demonstrates running the following command `C:\> & "C:\Program Files\dotnet\dotnet.exe" build --verbosity minimal --no-incremental C:\Build\DotNetCore.csproj` #> [CmdletBinding()] param( [Parameter(Mandatory)] # The `Whiskey.Context` object for the task running the command. [Whiskey.Context]$TaskContext, # The path to the `dotnet` executable to run the SDK command with. [String]$DotNetPath, [Parameter(Mandatory)] # The name of the .NET Core SDK command to run. [String]$Name, # A list of arguments to pass to the .NET Core SDK command. [String[]]$ArgumentList, # The path to a .NET Core solution or project file to pass to the .NET Core SDK command. [String]$ProjectPath, [switch] $NoLog ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if ( -not $DotNetPath ) { $DotNetPath = 'dotnet' } if ( -not (Get-Command -Name $DotNetPath -ErrorAction Ignore) ) { Write-WhiskeyError -Context $TaskContext -Message ('"{0}" does not exist.' -f $DotNetPath) return } $loggerArgs = @() if( -not $NoLog ) { $loggerArgs = & { '/filelogger9' $logFilePath = ('dotnet.{0}.log' -f $Name.ToLower()) if( $ProjectPath ) { $logFilePath = 'dotnet.{0}.{1}.log' -f $Name.ToLower(),($ProjectPath | Split-Path -Leaf) } $logFilePath = $logFilePath -replace '[^a-zA-Z0-9.]', '_' $relativeOutDirectory = $TaskContext.OutputDirectory | Resolve-Path -Relative $logFilePath = Join-Path -Path $relativeOutDirectory -ChildPath $logFilePath "/flp9:LogFile=$($logFilePath);Verbosity=d" } } $commandInfoArgList = & { $Name $ArgumentList $loggerArgs $ProjectPath } Write-WhiskeyCommand -Context $TaskContext -Path $DotNetPath -ArgumentList $commandInfoArgList Invoke-Command -ScriptBlock { param( $DotNetExe, $Command, $DotNetArgs, $LoggerArgs, $Project ) & $DotNetExe $Command $DotNetArgs $LoggerArgs $Project } -ArgumentList $DotNetPath,$Name,$ArgumentList,$loggerArgs,$ProjectPath if ($LASTEXITCODE -ne 0) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('"{0}" failed with exit code {1}' -f $DotNetPath,$LASTEXITCODE) return } } function Invoke-WhiskeyNpmCommand { <# .SYNOPSIS Runs `npm` with given command and argument. .DESCRIPTION The `Invoke-WhiskeyNpmCommand` function runs `npm` commands in the current workding directory. Pass the path to the build root to the `BuildRootPath` parameter. The function will use the copy of Node and NPM installed in the `.node` directory in the build root. Pass the name of the NPM command to run with the `Name` parameter. Pass any arguments to pass to the command with the `ArgumentList`. Task authors should add the `RequiresTool` attribute to their task functions to ensure that Whiskey installs Node and NPM, e.g. function MyTask { [Whiskey.Task('MyTask')] [Whiskey.RequiresTool('Node', PathParameterName='NodePath')] param( ) } .EXAMPLE Invoke-WhiskeyNpmCommand -Name 'install' -BuildRootPath $TaskParameter.BuildRoot -ForDeveloper:$Context.ByDeveloper Demonstrates how to run the `npm install` command from a task. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The NPM command to execute, e.g. `install`, `prune`, `run-script`, etc. [String]$Name, # An array of arguments to be given to the NPM command being executed. [String[]]$ArgumentList, [Parameter(Mandatory)] [String]$BuildRootPath, # NPM commands are being run on a developer computer. [switch]$ForDeveloper ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $BuildRootPath -ErrorAction Stop if( -not $nodePath ) { return } $npmPath = Resolve-WhiskeyNodeModulePath -Name 'npm' -BuildRootPath $BuildRootPath -Global -ErrorAction Stop $npmPath = Join-Path -Path $npmPath -ChildPath 'bin\npm-cli.js' if( -not $npmPath -or -not (Test-Path -Path $npmPath -PathType Leaf) ) { Write-WhiskeyError -Message ('Whiskey failed to install NPM. Something pretty serious has gone wrong.') return } # Assign to new variables otherwise Invoke-Command can't find them. $commandName = $Name $commandArgs = & { $ArgumentList '--scripts-prepend-node-path=auto' if( -not $ForDeveloper ) { '--no-color' } } $npmCommandString = ('npm {0} {1}' -f $commandName,($commandArgs -join ' ')) $originalPath = $env:PATH Set-Item -Path 'env:PATH' -Value ('{0}{1}{2}' -f (Split-Path -Path $nodePath -Parent),[IO.Path]::PathSeparator,$env:PATH) try { Write-Progress -Activity $npmCommandString Invoke-Command -ScriptBlock { # The ISE bails if processes write anything to STDERR. Node writes notices and warnings to # STDERR. We only want to stop a build if the command actually fails. $originalEap = $ErrorActionPreference if( $ErrorActionPreference -ne 'SilentlyContinue' ) { $ErrorActionPreference = 'Continue' } try { Write-WhiskeyCommand -Path $nodePath -ArgumentList $npmPath,$commandName,$commandArgs & $nodePath $npmPath $commandName $commandArgs } finally { Write-WhiskeyVerbose -Message ($LASTEXITCODE) $ErrorActionPreference = $originalEap } } if( $LASTEXITCODE -ne 0 ) { Write-WhiskeyError -Message ('NPM command "{0}" failed with exit code {1}. Please see previous output for more details.' -f $npmCommandString,$LASTEXITCODE) } } finally { Set-Item -Path 'env:PATH' -Value $originalPath Write-Progress -Activity $npmCommandString -Completed -PercentComplete 100 } } function Invoke-WhiskeyNuGetPush { [CmdletBinding()] param( [Parameter(Mandatory)] [String] $Path, [Parameter(Mandatory)] [String] $Url, [Parameter(Mandatory)] [String] $ApiKey, [Parameter(Mandatory)] [String] $NuGetPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState Write-WhiskeyCommand -Path $NuGetPath -ArgumentList $Path, $Url, $ApiKey & $NuGetPath push $Path -Source $Url -ApiKey $ApiKey } function Invoke-WhiskeyPipeline { <# .SYNOPSIS Invokes Whiskey pipelines. .DESCRIPTION The `Invoke-WhiskeyPipeline` function runs the tasks in a pipeline. Pipelines are properties in a `whiskey.yml` under which one or more tasks are defined. For example, this `whiskey.yml` file: Build: - TaskOne - TaskTwo Publish: - TaskOne - Task Defines two pipelines: `Build` and `Publish`. .EXAMPLE Invoke-WhiskeyPipeline -Context $context -Name 'Build' Demonstrates how to run the tasks in a `Build` pipeline. The `$context` object is created by calling `New-WhiskeyContext`. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The current build context. Use the `New-WhiskeyContext` function to create a context object. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The name of pipeline to run, e.g. `Build` would run all the tasks under a property named `Build`. Pipelines are properties in your `whiskey.yml` file that are lists of Whiskey tasks to run. [String]$Name ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $config = $Context.Configuration $Context.PipelineName = $Name if( -not $config.ContainsKey($Name) ) { Stop-Whiskey -Context $Context -Message ('Pipeline "{0}" does not exist. Create a pipeline by defining a "{0}" property: {0}: - TASK_ONE - TASK_TWO ' -f $Name) return } $taskIdx = -1 if( -not $config[$Name] ) { Write-WhiskeyWarning -Context $Context -Message ('It looks like pipeline "{0}" doesn''t have any tasks.' -f $Name) $config[$Name] = @() } foreach( $taskItem in $config[$Name] ) { $taskIdx++ $taskName,$taskParameter = ConvertTo-WhiskeyTask -InputObject $taskItem -ErrorAction Stop if( -not $taskName ) { continue } $Context.TaskIndex = $taskIdx Invoke-WhiskeyTask -TaskContext $Context -Name $taskName -Parameter $taskParameter } } function Invoke-WhiskeyRobocopy { [CmdletBinding()] param( [Parameter(Mandatory)] [String] $Source, [Parameter(Mandatory)] [String] $Destination, [String[]] $WhiteList, [String[]] $Exclude ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $numRobocopyThreads = Get-CimInstance -ClassName 'Win32_Processor' | Select-Object -ExpandProperty 'NumberOfLogicalProcessors' | Measure-Object -Sum | Select-Object -ExpandProperty 'Sum' $numRobocopyThreads *= 2 $logPathFileName = "robocopy-$([IO.Path]::GetRandomFileName() -replace '\.','').log" $logPath = Join-Path -Path (Get-WhiskeyTempPath) -ChildPath $logPathFileName $excludeParam = $Exclude | ForEach-Object { '/XF' ; $_ ; '/XD' ; $_ } robocopy $Source ` $Destination ` '/PURGE' ` '/S' ` '/R:0' ` "/LOG:${logPath}" ` "/MT:${numRobocopyThreads}" ` $WhiteList ` $excludeParam try { if ($LASTEXITCODE -ge 8) { Get-Content -Path $logPath $msg = "The command ""robocopy.exe '${Source}' '${Destination}'"" failed with exit code ${LASTEXITCODE}." Write-WhiskeyError $msg return } # Make sure one of Robocopy's success exit codes doesn't fail the build. $Global:LASTEXITCODE = $LASTEXITCODE = 0 } finally { if (Test-Path -Path $logPath) { Remove-Item -Path $logPath -Force } } } function Invoke-WhiskeyTask { <# .SYNOPSIS Runs a Whiskey task. .DESCRIPTION The `Invoke-WhiskeyTask` function runs a Whiskey task. #> [CmdletBinding()] param( # The context this task is operating in. Use `New-WhiskeyContext` to create context objects. [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, # The name of the task. [Parameter(Mandatory)] [String] $Name, # The parameters/configuration to use to run the task. [Parameter(Mandatory)] [hashtable] $Parameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState function Invoke-Event { param( $EventName, $Property ) $events = $TaskContext.Events if( -not $events.ContainsKey($EventName) ) { return } foreach( $commandName in $events[$EventName] ) { Write-WhiskeyVerbose -Context $TaskContext -Message '' Write-WhiskeyVerbose -Context $TaskContext -Message ('[On{0}] {1}' -f $EventName,$commandName) $startedAt = Get-Date $result = 'FAILED' try { $TaskContext.Temp = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('Temp.{0}.On{1}.{2}' -f $Name,$EventName,[IO.Path]::GetRandomFileName()) if( -not (Test-Path -Path $TaskContext.Temp -PathType Container) ) { New-Item -Path $TaskContext.Temp -ItemType 'Directory' -Force | Out-Null } & $commandName -TaskContext $TaskContext -TaskName $Name -TaskParameter $Property $result = 'COMPLETED' } finally { Remove-WhiskeyFileSystemItem -Path $TaskContext.Temp $endedAt = Get-Date $duration = $endedAt - $startedAt Write-WhiskeyVerbose -Context $TaskContext ('{0} {1} in {2}' -f (' ' * ($EventName.Length + 4)),$result,$duration) Write-WhiskeyVerbose -Context $TaskContext -Message '' } } } function Merge-Parameter { param( [hashtable]$SourceParameter, [hashtable]$TargetParameter ) foreach( $key in $SourceParameter.Keys ) { $sourceValue = $SourceParameter[$key] if( $TargetParameter.ContainsKey($key) ) { $targetValue = $TargetParameter[$key] if( ($targetValue | Get-Member -Name 'Keys') -and ($sourceValue | Get-Member -Name 'Keys') ) { Merge-Parameter -SourceParameter $sourceValue -TargetParameter $targetValue } continue } $TargetParameter[$key] = $sourceValue } } function Get-RequiredTool { param( $CommandName ) $cmd = Get-Command -Name $CommandName -ErrorAction Ignore if( -not $cmd -or -not (Get-Member -InputObject $cmd -Name 'ScriptBlock') ) { return } $cmd.ScriptBlock.Attributes | Where-Object { $_ -is [Whiskey.RequiresToolAttribute] } } $knownTasks = Get-WhiskeyTask -Force $task = $knownTasks | Where-Object { $_.Name -eq $Name } if( -not $task ) { $task = $knownTasks | Where-Object { $_.Aliases -contains $Name } $taskCount = ($task | Measure-Object).Count if( $taskCount -gt 1 ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Found {0} tasks with alias "{1}". Please update to use one of these task names: {2}.' -f $taskCount,$Name,(($task | Select-Object -ExpandProperty 'Name') -join ', ')) return } if( $task -and $task.WarnWhenUsingAlias ) { Write-WhiskeyWarning -Context $TaskContext -Message ('Task "{0}" is an alias to task "{1}". Please update "{2}" to use the task''s actual name, "{1}", instead of the alias.' -f $Name,$task.Name,$TaskContext.ConfigurationPath) } } if( -not $task ) { $knownTaskNames = $knownTasks | Select-Object -ExpandProperty 'Name' | Sort-Object throw ('{0}: {1}[{2}]: ''{3}'' task does not exist. Supported tasks are:{4} * {5}' -f $TaskContext.ConfigurationPath,$Name,$TaskContext.TaskIndex,$Name,[Environment]::NewLine,($knownTaskNames -join ('{0} * ' -f [Environment]::NewLine))) } $taskCount = ($task | Measure-Object).Count if( $taskCount -gt 1 ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Found {0} tasks named "{1}". We don''t know which one to use. Please make sure task names are unique.' -f $taskCount,$Name) return } if( $task.Obsolete ) { $message = 'The "{0}" task is obsolete and shouldn''t be used.' -f $Name if( $task.ObsoleteMessage ) { $message = $task.ObsoleteMessage } Write-WhiskeyWarning -Context $TaskContext -Message $message } if( -not $task.Platform.HasFlag($CurrentPlatform) ) { $msg = 'Unable to run task "{0}": it is only supported on the {1} platform(s) and we''re currently running on {2}.' -f ` $Name,$task.Platform,$CurrentPlatform Write-WhiskeyError -Message $msg -ErrorAction Stop return } if( $TaskContext.TaskDefaults.ContainsKey( $Name ) ) { Merge-Parameter -SourceParameter $TaskContext.TaskDefaults[$Name] -TargetParameter $Parameter } Resolve-WhiskeyVariable -Context $TaskContext -InputObject $Parameter | Out-Null [hashtable]$taskProperties = $Parameter.Clone() $commonProperties = @{} foreach( $commonPropertyName in @( 'OnlyBy', 'ExceptBy', 'OnlyOnBranch', 'ExceptOnBranch', 'OnlyDuring', 'ExceptDuring', 'OnlyOnPlatform', 'ExceptOnPlatform', 'IfExists', 'UnlessExists', 'WorkingDirectory', 'OutVariable' ) ) { if ($taskProperties.ContainsKey($commonPropertyName)) { $commonProperties[$commonPropertyName] = $taskProperties[$commonPropertyName] $taskProperties.Remove($commonPropertyName) } } # Start every task in the BuildRoot. Push-Location $TaskContext.BuildRoot $originalDirectory = [IO.Directory]::GetCurrentDirectory() [IO.Directory]::SetCurrentDirectory($TaskContext.BuildRoot) try { if( Test-WhiskeyTaskSkip -Context $TaskContext -Properties $commonProperties) { $result = 'SKIPPED' return } $inCleanMode = $TaskContext.ShouldClean if( $inCleanMode ) { if( -not $task.SupportsClean ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('SupportsClean.{0} -ne Build.ShouldClean.{1}' -f $task.SupportsClean,$TaskContext.ShouldClean) $result = 'SKIPPED' return } } $requiredTools = Get-RequiredTool -CommandName $task.CommandName foreach( $requiredTool in $requiredTools ) { Install-WhiskeyTool -ToolInfo $requiredTool ` -InstallRoot $TaskContext.BuildRoot.FullName ` -TaskParameter $taskProperties ` -OutFileRootPath $TaskContext.OutputDirectory.FullName ` -InCleanMode:$inCleanMode ` -ErrorAction Stop } if( $TaskContext.ShouldInitialize -and -not $task.SupportsInitialize ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('SupportsInitialize.{0} -ne Build.ShouldInitialize.{1}' -f $task.SupportsInitialize,$TaskContext.ShouldInitialize) $result = 'SKIPPED' return } $taskTempDirectory = '' $result = 'FAILED' $originalDebugPreference = $DebugPreference try { $workingDirectory = $TaskContext.BuildRoot if( $Parameter['WorkingDirectory'] ) { # We need a full path because we pass it to `IO.Path.SetCurrentDirectory`. $workingDirectory = $Parameter['WorkingDirectory'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'WorkingDirectory' -Mandatory -OnlySinglePath -PathType 'Directory' | Resolve-Path | Select-Object -ExpandProperty 'ProviderPath' } Set-Location -Path $workingDirectory [IO.Directory]::SetCurrentDirectory($workingDirectory) Invoke-Event -EventName 'BeforeTask' -Property $taskProperties Invoke-Event -EventName ('Before{0}Task' -f $Name) -Property $taskProperties Write-WhiskeyVerbose -Context $TaskContext -Message '' $TaskContext.StartTask($Name) Write-WhiskeyInfo -Context $TaskContext -Message "$($Name)" -NoIndent $taskTempDirectory = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('Temp.{0}.{1}' -f $Name,[IO.Path]::GetRandomFileName()) $TaskContext.Temp = $taskTempDirectory if( -not (Test-Path -Path $TaskContext.Temp -PathType Container) ) { New-Item -Path $TaskContext.Temp -ItemType 'Directory' -Force | Out-Null } $taskArgs = Get-TaskArgument -Name $task.CommandName -Property $taskProperties -Context $TaskContext # PowerShell's default DebugPreference when someone uses the -Debug switch is `Inquire`. That would cause a # build to hang, so let's set it to Continue so users can see debug output. if( $taskArgs['Debug'] ) { $DebugPreference = 'Continue' $taskArgs.Remove('Debug') } $outVariable = $commonProperties['OutVariable'] if ($outVariable) { $taskOutput = & $task.CommandName @taskArgs if (-not $taskOutput) { $taskOutput = '' } Add-WhiskeyVariable -Context $TaskContext -Name $outVariable -Value $taskOutput } else { & $task.CommandName @taskArgs } $result = 'COMPLETED' } finally { $DebugPreference = $originalDebugPreference # Clean required tools *after* running the task since the task might need a required tool in order to do the cleaning (e.g. using Node to clean up installed modules) if( $TaskContext.ShouldClean ) { foreach( $requiredTool in $requiredTools ) { Uninstall-WhiskeyTool -BuildRoot $TaskContext.BuildRoot -ToolInfo $requiredTool } } if( $taskTempDirectory -and (Test-Path -Path $taskTempDirectory -PathType Container) ) { Remove-Item -Path $taskTempDirectory -Recurse -Force -ErrorAction Ignore } $Context.StopTask() $duration = $Context.TaskStopwatch.Elapsed if( $result -eq 'FAILED' ) { $msg = "!$($taskWriteIndent.Substring(1))FAILED" Write-WhiskeyInfo -Context $TaskContext -Message $msg -NoIndent } Write-WhiskeyInfo -Context $TaskContext -Message '' -NoIndent } Invoke-Event -EventName 'AfterTask' -Property $taskProperties Invoke-Event -EventName ('After{0}Task' -f $Name) -Property $taskProperties } finally { [IO.Directory]::SetCurrentDirectory($originalDirectory) Pop-Location } } function New-WhiskeyBuildMetadataObject { [CmdletBinding()] [OutputType([Whiskey.BuildInfo])] param( ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState return New-Object -TypeName 'Whiskey.BuildInfo' } function New-WhiskeyContext { <# .SYNOPSIS Creates a context object to use when running builds. .DESCRIPTION The `New-WhiskeyContext` function creates a `Whiskey.Context` object used when running builds. It: * Reads in the whiskey.yml file containing the build you want to run. * Creates a ".output" directory in the same directory as your whiskey.yml file for storing build output, logs, results, temp files, etc. * Reads build metadata created by the current build server (if being run by a build server). * Sets the version number to "0.0.0". ## Whiskey.Context The `Whiskey.Context` object has the following properties. ***Do not use any property not defined below.*** Also, these properties are ***read-only***. If you write to them, Bad Things (tm) could happen. * `BuildMetadata`: a `Whiskey.BuildInfo` object representing build metadata provided by the build server. * `BuildRoot`: a `System.IO.DirectoryInfo` object representing the directory the YAML configuration file is in. * `ByBuildServer`: a flag indicating if the build is being run by a build server. * `ByDeveloper`: a flag indicating if the build is being run by a developer. * `Environment`: the environment the build is running in. * `OutputDirectory`: a `System.IO.DirectoryInfo` object representing the path to a directory where build output, reports, etc. should be saved. This directory is created for you. * `ShouldClean`: a flag indicating if the current build is running in clean mode. * `ShouldInitialize`: a flag indicating if the current build is running in initialize mode. * `Temp`: the temporary work directory for the current task. * `Version`: a `Whiskey.BuildVersion` object representing version being built (see below). Any other property is considered private and may be removed, renamed, and/or reimplemented at our discretion without notice. ## Whiskey.BuildInfo The `Whiskey.BuildInfo` object has the following properties. ***Do not use any property not defined below.*** Also, these properties are ***read-only***. If you write to them, Bad Things (tm) could happen. * `BuildNumber`: the current build number. This comes from the build server. (If the build is being run by a developer, this is always "0".) It increments with every new build (or should). This number is unique only to the current build job. * `ScmBranch`: the branch name from which the current build is running. * `ScmCommitID`: the unique commit ID from which the current build is running. The commit ID distinguishes the current commit from all others in the source repository and is the same across copies of a repository. ## Whiskey.BuildVersion The `Whiskey.BuildVersion` object has the following properties. ***Do not use any property not defined below.*** Also, these properties are ***read-only***. If you write to them, Bad Things (tm) could happen. * `SemVer2`: the version currently being built. * `Version`: a `System.Version` object for the current build. Only major, minor, and patch/build numbers will be filled in. * `SemVer1`: a semver version 1 compatible version of the current build. * `SemVer2NoBuildMetadata`: the current version without any build metadata. .EXAMPLE New-WhiskeyContext -Path '.\whiskey.yml' Demonstrates how to create a context for a developer build. #> [CmdletBinding()] [OutputType([Whiskey.Context])] param( # The environment you're building in. [Parameter(Mandatory)] [String] $Environment, # The path to the `whiskey.yml` file that defines build settings and tasks. [Parameter(Mandatory)] [String] $ConfigurationPath, # The place where downloaded tools should be cached. The default is the build root. [String] $DownloadRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $ConfigurationPath = Resolve-Path -LiteralPath $ConfigurationPath -ErrorAction Ignore if( -not $ConfigurationPath ) { throw ('Configuration file path ''{0}'' does not exist.' -f $PSBoundParameters['ConfigurationPath']) } $config = Import-WhiskeyYaml -Path $ConfigurationPath if( $config.ContainsKey('Build') -and $config.ContainsKey('BuildTasks') ) { throw ('{0}: The configuration file contains both "Build" and the deprecated "BuildTasks" pipelines. Move all your build tasks under "Build" and remove the "BuildTasks" pipeline.' -f $ConfigurationPath) } if( $config.ContainsKey('BuildTasks') ) { Write-WhiskeyWarning ('{0}: The default "BuildTasks" pipeline has been renamed to "Build". Backwards compatibility with "BuildTasks" will be removed in the next major version of Whiskey. Rename your "BuildTasks" pipeline to "Build".' -f $ConfigurationPath) } if( $config.ContainsKey('Publish') -and $config.ContainsKey('PublishTasks') ) { throw ('{0}: The configuration file contains both "Publish" and the deprecated "PublishTasks" pipelines. Move all your publish tasks under "Publish" and remove the "PublishTasks" pipeline.' -f $ConfigurationPath) } if( $config.ContainsKey('PublishTasks') ) { Write-WhiskeyWarning ('{0}: The default "PublishTasks" pipeline has been renamed to "Publish". Backwards compatibility with "PublishTasks" will be removed in the next major version of Whiskey. Rename your "PublishTasks" pipeline to "Publish".' -f $ConfigurationPath) } $buildRoot = $ConfigurationPath | Split-Path if( -not $DownloadRoot ) { $DownloadRoot = $buildRoot } [Whiskey.BuildInfo]$buildMetadata = Get-WhiskeyBuildMetadata $publish = $false $byBuildServer = $buildMetadata.IsBuildServer if( $byBuildServer ) { $branch = $buildMetadata.ScmBranch if( -not $buildMetadata.IsPullRequest -and $config.ContainsKey('PublishOn') ) { Write-WhiskeyVerbose -Message ('PublishOn') foreach( $publishWildcard in $config['PublishOn'] ) { $publish = $branch -like $publishWildcard if( $publish ) { Write-WhiskeyVerbose -Message (' {0} -like {1}' -f $branch,$publishWildcard) break } else { Write-WhiskeyVerbose -Message (' {0} -notlike {1}' -f $branch,$publishWildcard) } } } } $outputDirectory = Join-Path -Path $buildRoot -ChildPath '.output' if( -not (Test-Path -Path $outputDirectory -PathType Container) ) { New-Item -Path $outputDirectory -ItemType 'Directory' -Force | Out-Null } $context = New-WhiskeyContextObject $context.BuildRoot = $buildRoot $runBy = [Whiskey.RunBy]::Developer if( $byBuildServer ) { $runBy = [Whiskey.RunBy]::BuildServer } $context.RunBy = $runBy $context.BuildMetadata = $buildMetadata $context.Configuration = $config $context.ConfigurationPath = $ConfigurationPath $context.DownloadRoot = $DownloadRoot $context.Environment = $Environment $context.OutputDirectory = $outputDirectory $context.Publish = $publish $context.RunMode = [Whiskey.RunMode]::Build if( $config['Variable'] ) { Write-WhiskeyError -Message ('{0}: The "Variable" property is no longer supported. Use the `SetVariable` task instead. Move your `Variable` property (and values) into your `Build` pipeline as the first task. Rename `Variable` to `SetVariable`.' -f $ConfigurationPath) -ErrorAction Stop } $context.Version = New-WhiskeyVersionObject -SemVer '0.0.0' return $context } function New-WhiskeyContextObject { [CmdletBinding()] [OutputType([Whiskey.Context])] param( ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState return New-Object -TypeName 'Whiskey.Context' } function New-WhiskeyVersionObject { [CmdletBinding()] [OutputType([Whiskey.BuildVersion])] param( [SemVersion.SemanticVersion]$SemVer ) $whiskeyVersion = New-Object -TypeName 'Whiskey.BuildVersion' if( $SemVer ) { $major = $SemVer.Major $minor = $SemVer.Minor $patch = $SemVer.Patch $prerelease = $SemVer.Prerelease $build = $SemVer.Build $version = New-Object -TypeName 'Version' -ArgumentList $major,$minor,$patch $semVersionNoBuild = New-Object -TypeName 'SemVersion.SemanticVersion' -ArgumentList $major,$minor,$patch $semVersionV1 = New-Object -TypeName 'SemVersion.SemanticVersion' -ArgumentList $major,$minor,$patch if( $prerelease ) { $semVersionNoBuild = New-Object -TypeName 'SemVersion.SemanticVersion' -ArgumentList $major,$minor,$patch,$prerelease $semVersionV1Prerelease = $prerelease -replace '[^A-Za-z0-90]','' $semVersionV1 = New-Object -TypeName 'SemVersion.SemanticVersion' -ArgumentList $major,$minor,$patch,$semVersionV1Prerelease } $whiskeyVersion.Version = $version $whiskeyVersion.SemVer2 = $SemVer $whiskeyVersion.SemVer2NoBuildMetadata = $semVersionNoBuild $whiskeyVersion.SemVer1 = $semVersionV1 } return $whiskeyVersion } function Publish-WhiskeyPesterTestResult { [CmdletBinding()] param( [Parameter(Mandatory)] # The path to the Pester test resut. [String]$Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not (Test-Path -Path 'env:APPVEYOR_JOB_ID') ) { return } $webClient = New-Object 'Net.WebClient' $uploadUri = 'https://ci.appveyor.com/api/testresults/nunit/{0}' -f $env:APPVEYOR_JOB_ID Resolve-Path -Path $Path -ErrorAction Stop | Select-Object -ExpandProperty 'ProviderPath' | ForEach-Object { $resultPath = $_ Write-WhiskeyVerbose -Message ('Uploading Pester test result file ''{0}'' to AppVeyor at ''{1}''.' -f $resultPath,$uploadUri) $webClient.UploadFile($uploadUri, $resultPath) } } function Publish-WhiskeyPSObject { [CmdletBinding()] param( [Parameter(Mandatory)] [Object] $Context, [Parameter(Mandatory, ParameterSetName='Module')] [Management.Automation.PSModuleInfo] $ModuleInfo, [Parameter(Mandatory, ParameterSetName='Script')] [PSCustomObject] $ScriptInfo, [String] $RepositoryName, [String] $RepositoryLocation, [String] $CredentialID, [String] $ApiKeyID ) $commonParams = @{} if( $VerbosePreference -in @('Continue','Inquire') ) { $commonParams['Verbose'] = $true } if( (Test-Path -Path 'variable:InformationPreference') ) { $commonParams['InformationAction'] = $InformationPreference } Write-WhiskeyDebug -Context $TaskContext -Message 'Bootstrapping NuGet packageprovider.' Get-PackageProvider -Name 'NuGet' -ForceBootstrap @commonParams | Out-Null $createTempRepo = $false $infoMsg = '' if( -not $RepositoryLocation -and -not $RepositoryName ) { $createTempRepo = $true $RepositoryLocation = $TaskContext.OutputDirectory.FullName $infoMsg = """$($RepositoryLocation | Resolve-WhiskeyRelativePath)""" } elseif( $RepositoryLocation ) { $publishTo = Get-PSRepository -ErrorAction Ignore @commonParams | Where-Object 'PublishLocation' -eq $RepositoryLocation if( $publishTo ) { $RepositoryName = $publishTo.Name } else { $createTempRepo = $true } } elseif( $RepositoryName ) { $publishTo = Get-PSRepository -ErrorAction Ignore @commonParams | Where-Object 'Name' -eq $RepositoryName if( -not $publishTo ) { Get-PSRepository | Format-Table -AutoSize if( $ScriptInfo ) { $msg = "Unable to publish PowerShell script ""$($ScriptInfo.ScriptBase | Resolve-WhiskeyRelativePath)"" to " + "repository ""$($RepositoryName)"": a repository with that name doesn't exist. Update your " + 'PublishPowerShellScript task with the name of one of the repository''s that ' + 'exists (see above), use the "RepositoryLocation" to specify the URI or path to a repository, ' + 'or leave "RepositoryName" blank to publish to the build output directory.' } else { $msg = "Unable to publish PowerShell module ""$($ModuleInfo.ModuleBase | Resolve-WhiskeyRelativePath)"" to " + "repository ""$($RepositoryName)"": a repository with that name doesn't exist. Update your " + 'PublishPowerShellModule task with the name of one of the repository''s that ' + 'exists (see above), use the "RepositoryLocation" to specify the URI or path to a repository, ' + 'or leave "RepositoryName" blank to publish to the build output directory.' } Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $RepositoryLocation = $publishTo.PublishLocation } try { if( $createTempRepo ) { $credentialParam = @{ } if( $CredentialID ) { $credentialParam['Credential'] = Get-WhiskeyCredential -Context $TaskContext -ID $CredentialID -PropertyName 'CredentialID' } $tempNameSuffix = [IO.Path]::GetRandomFileName() -replace '\.', '' $RepositoryName = "Whiskey-$($TaskContext.BuildRoot.FullName)-$($tempNameSuffix)" $msg = "Registering PowerShell repository ""$($RepositoryName)"" at ""$($RepositoryLocation)""." Write-WhiskeyVerbose -Context $TaskContext -Message $msg # Do *not* ErrorAction Stop this. It causes a handled error deep in the bowels of PackageManagement to # cause Register-PSRepository to fail. Register-PSRepository -Name $RepositoryName ` -SourceLocation $RepositoryLocation ` -PublishLocation $RepositoryLocation ` -InstallationPolicy Trusted ` -PackageManagementProvider NuGet ` -ErrorAction Continue ` @credentialParam ` @commonParams if( -not (Get-PSRepository -Name $RepositoryName) ) { Get-PSRepository | Format-Table -Auto $msg = "Register-PSRepository didn't register ""$($RepositoryName)"" at location " + """$($RepositoryLocation)""." Stop-WhiskeyTask -TaskContext $Context -Message $msg return } } $apiKeyParam = @{} if( $ApiKeyID ) { $apiKey = Get-WhiskeyApiKey -Context $TaskContext -ID $ApiKeyID -PropertyName 'ApiKeyID' if( $apiKey ) { $apiKeyParam['NuGetApiKey'] = $apiKey } } if( -not $infoMsg ) { $infoMsg = "repository ""$($RepositoryName)"" at ""$($RepositoryLocation)""" } if( $ScriptInfo ) { $msg = "Publishing PowerShell script ""$($Path | Resolve-WhiskeyRelativePath)"" to $($infoMsg)." } else { $msg = "Publishing PowerShell module ""$($Path | Resolve-WhiskeyRelativePath)"" to $($infoMsg)." } Write-WhiskeyInfo -Context $TaskContext -Message $msg Get-Module | Format-Table -AutoSize | Out-String | Write-Debug # Use the Force switch to allow publishing versions that come *before* the latest version. if( $ScriptInfo ) { Publish-Script -Path $Path -Repository $RepositoryName -Force @apiKeyParam @commonParams -ErrorAction Stop } else { Publish-Module -Path $Path -Repository $RepositoryName -Force @apiKeyParam @commonParams -ErrorAction Stop } } finally { if( $createTempRepo ) { $msg = "Unregistering temporary PowerShell repository ""$($RepositoryName)""." Write-WhiskeyVerbose -Context $TaskContext -Message $msg Unregister-PSRepository -Name $RepositoryName } } } function Register-WhiskeyEvent { <# .SYNOPSIS Registers a command to call when specific events happen during a build. .DESCRIPTION The `Register-WhiskeyEvent` function registers a command to run when a specific event happens during a build. Supported events are: * `BeforeTask` which runs before each task * `AfterTask`, which runs after each task `BeforeTask` and `AfterTask` event handlers must have the following parameters: function Invoke-WhiskeyTaskEvent { param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [String]$TaskName, [Parameter(Mandatory)] [hashtable]$TaskParameter ) } To stop a build while handling an event, call the `Stop-WhiskeyTask` function. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The context where the event should fire. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The name of the command to run during the event. [String]$CommandName, [Parameter(Mandatory)] [ValidateSet('BeforeTask','AfterTask')] # When the command should be run; what events does it respond to? [String]$Event, # Only fire the event for a specific task. [String]$TaskName ) Set-StrictMode -Version 'Latest' $eventName = $Event if( $TaskName ) { $eventType = $Event -replace 'Task$','' $eventName = '{0}{1}Task' -f $eventType,$TaskName } $events = $Context.Events if( -not $events[$eventName] ) { $events[$eventName] = New-Object -TypeName 'Collections.Generic.List[String]' } $events[$eventName].Add( $CommandName ) } function Register-WhiskeyPSModulePath { # If there are older versions of the PackageManagement and/or PowerShellGet # modules available on this system, the modules that ship with Whiskey will use # those global versions instead of the versions we load from inside Whiskey. So, # we have to put the ones that ship with Whiskey first. See # https://github.com/PowerShell/PowerShellGet/issues/55 . [CmdletBinding()] param( [Parameter(Mandatory,ParameterSetName='FromUser')] [String]$Path, [Parameter(Mandatory,ParameterSetName='FromWhiskey')] [String]$PSModulesRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-WhiskeyDebug '\Register-WhiskeyPSModulePath\' -Indent try { if( $PSCmdlet.ParameterSetName -eq 'FromWhiskey' ) { $Path = Get-WhiskeyPSModulePath -PSModulesRoot $PSModulesRoot } $pathBefore = $env:PSModulePath -split [IO.Path]::PathSeparator try { if( $pathBefore -contains $Path ) { return } $env:PSModulePath = $Path,$env:PSModulePath -join [IO.Path]::PathSeparator } finally { Write-WhiskeyDebug "Changes to PSModulePath:" $pathNow = $env:PSModulePath -split [IO.Path]::PathSeparator $diff = Compare-Object -ReferenceObject $pathBefore -DifferenceObject $pathNow -IncludeEqual if( $diff ) { $diff | Format-Table -AutoSize | Out-String | Write-WhiskeyDebug } } } finally { Write-WhiskeyDebug '/Register-WhiskeyPSModulePath/' -Outdent } } function Remove-WhiskeyFileSystemItem { <# .SYNOPSIS Deletes a file or directory. .DESCRIPTION The `Remove-WhiskeyFileSystemItem` deletes files and directories. Directories are deleted recursively. On Windows, this function uses robocopy to delete directories, since it can handle files/directories whose paths are longer than the maximum 260 characters. If the file or directory doesn't exist, nothing happens. The path to delete should be absolute or relative to the current working directory. This function won't fail a build. If you want it to fail a build, pass the `-ErrorAction Stop` parameter. .EXAMPLE Remove-WhiskeyFileSystemItem -Path 'C:\some\file' Demonstrates how to delete a file. .EXAMPLE Remove-WhiskeyFilesystemItem -Path 'C:\project\node_modules' Demonstrates how to delete a directory. .EXAMPLE Remove-WhiskeyFileSystemItem -Path 'C:\project\node_modules' -ErrorAction Stop Demonstrates how to fail a build if the delete fails. #> [CmdletBinding()] param( [Parameter(Mandatory)] [String]$Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-WhiskeyDebug -Message ('Remove-WhiskeyFileSystemItem BEGIN {0}' -f $Path) if( (Test-Path -Path $Path -PathType Leaf) ) { Remove-Item -Path $Path -Force } elseif( (Test-Path -Path $Path -PathType Container) ) { if( $IsWindows ) { $emptyDir = Get-WhiskeyTempPath -Name 'Empty' try { Invoke-WhiskeyRobocopy -Source $emptyDir -Destination $Path Remove-Item -Path $Path -Recurse -Force } finally { if (Test-Path -Path $emptyDir) { Remove-Item -Path $emptyDir -Recurse -Force } } } else { Remove-Item -Path $Path -Recurse -Force } } Write-WhiskeyDebug -Message ('Remove-WhiskeyFileSystemItem END') } function Resolve-WhiskeyDotNetSdkVersion { <# .SYNOPSIS Searches for a version of the .NET Core SDK to ensure it exists and returns the resolved version. .DESCRIPTION The `Resolve-WhiskeyDotNetSdkVersion` function ensures a given version is a valid released version of the .NET Core SDK. By default, the function will return the latest LTS version of the SDK. If a `Version` number is given then that version is compared against the list of released SDK versions to ensure the given version is valid. If no valid version is found matching `Version`, then an error is written and nothing is returned. The logic for the provided RollForward value is as follows: For `Patch`, `Feature`, `Major`, and `Minor`, the most recent patch for the specified versions is returned. For `LatestPatch`, the latest patch for the specified major, minor, and feature versions is used. For `LatestFeature`, the latest patch and feature is used for the provided major and minor versions. For `LatestMinor`, the latest minor, feature, and patch are used for the specified major version. For `LatestMajor`, the most recently released version of the .NET Core SDK is used. .EXAMPLE Resolve-WhiskeyDotNetSdkVersion -LatestLTS Demonstrates returning the latest LTS version of the .NET Core SDK. .EXAMPLE Resolve-WhiskeyDotNetSdkVersion -Version '2.1.2' Demonstrates ensuring that version '2.1.2' is a valid released version of the .NET Core SDK. .EXAMPLE Resolve-WhiskeyDotNetSdkVersion -Version '2.*' Demonstrates resolving the latest '2.x.x' version of the .NET Core SDK. .EXAMPLE Resolve-WhiskeyDotNetSdkVersion -Version '2.1.2' -RollForward Patch Demonstrates finding the latest '2.1.x' version of the .NET Core SDK. #> [CmdletBinding(DefaultParameterSetName='LatestLTS')] param( [Parameter(ParameterSetName='LatestLTS')] # Returns the latest LTS version of the .NET Core SDK. [switch] $LatestLTS, [Parameter(Mandatory, ParameterSetName='Version')] # Version of the .NET Core SDK to search for and resolve. Accepts wildcards. [String] $Version, # Roll forward preferences for the .NET Core SDK [Parameter(ParameterSetName='Version')] [WhiskeyDotNetSdkRollForward] $RollForward = [WhiskeyDotNetSdkRollForward]::Disable ) Set-StrictMode -version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue if ( $LatestLTS ) { $latestLTSVersionUri = 'https://dotnetcli.blob.core.windows.net/dotnet/Sdk/LTS/latest.version' Write-WhiskeyVerbose -Message ('[{0}] Resolving latest LTS version of .NET Core SDK from: "{1}"' -f $MyInvocation.MyCommand,$latestLTSVersionUri) $latestLTSVersion = Invoke-RestMethod -Uri $latestLTSVersionUri -ErrorAction Stop if ($latestLTSVersion -match '(\d+\.\d+\.\d+)') { $resolvedVersion = $Matches[1] } else { Write-WhiskeyError -Message ('Could not retrieve the latest LTS version of the .NET Core SDK. "{0}" returned:{1}{2}' -f $latestLTSVersionUri,[Environment]::NewLine,$latestLTSVersion) return } Write-WhiskeyVerbose -Message ('[{0}] Latest LTS version resolved as: "{1}"' -f $MyInvocation.MyCommand,$resolvedVersion) return $resolvedVersion } $urisToTry = @( 'https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json', 'https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases-index.json' ) $releasesIndex = $null foreach( $uri in $urisToTry ) { $releasesIndex = Invoke-RestMethod -Uri $uri -ErrorAction Ignore | Select-Object -ExpandProperty 'releases-index' -ErrorAction Ignore if( $releasesIndex ) { $releasesIndexUri = $uri break } } if( -not $releasesIndex ) { Write-WhiskeyError -Message ('Unable to find the .NET Core releases index. We tried each of these URIs:{0} {0}* {1}{0} ' -f [Environment]::NewLine,($urisToTry -join ('{0}* ' -f [Environment]::NewLine))) return } $releasesIndex = $releasesIndex | Where-Object { [Version]::TryParse($_.'channel-version', [ref]$null) } | ForEach-Object { $_.'channel-version' = [Version]$_.'channel-version' $_ } | Sort-Object -Property 'channel-version' -Descending if ($Version -match '^\*|^\d+\.\*') { $matcher = $Matches[0] } elseif ($Version -match '^(\d+)\.(\d+)') { switch ($RollForward) { LatestMajor { $matcher = '*' } LatestMinor { $matcher = "$($Matches[1]).*" } default { $matcher = "$($Matches[1]).$($Matches[2])" } } } $release = $releasesIndex | Sort-Object -Property 'channel-version' -Descending | Where-Object { $_.'channel-version' -like $matcher } | Select-Object -First 1 if (-not $release -and $RollForward -eq [WhiskeyDotNetSdkRollForward]::Disable) { Write-WhiskeyError -Message ('.NET Core release matching "{0}" could not be found in "{1}"' -f $matcher, $releasesIndexUri) return } $releasesJsonUri = $release | Select-Object -ExpandProperty 'releases.json' Write-WhiskeyVerbose -Message ('[{0}] Resolving .NET Core SDK version "{1}" against known released versions at: "{2}"' -f $MyInvocation.MyCommand,$Version,$releasesJsonUri) $releasesJson = Invoke-RestMethod -Uri $releasesJsonUri -ErrorAction Stop $sdkVersions = & { $releasesJson.releases | Where-Object { $_ | Get-Member -Name 'sdk' } | Select-Object -ExpandProperty 'sdk' | Select-Object -ExpandProperty 'version' $releasesJson.releases | Where-Object { $_ | Get-Member -Name 'sdks' } | Select-Object -ExpandProperty 'sdks' | Select-Object -ExpandProperty 'version' } $desiredVersion = $null $sortedSdkVersions = $null if ([WildcardPattern]::ContainsWildcardCharacters($Version)) { $resolvedVersion = $sdkVersions | Where-Object { $_ -like $Version} | Sort-Object -Descending | Select-Object -First 1 Write-WhiskeyVerbose -Message ('[{0}] SDK version "{1}" resolved to "{2}"' -f $MyInvocation.MyCommand, $Version, $resolvedVersion) return $resolvedVersion } if ( $Version -notmatch '^(\d+)\.(\d+)\.(\d{1})(\d+)' ) { $msg = ".NET SDK version ""$($Version)"" is invalid. The SDK version must be in the form of a 3-part version " + "number. See https://learn.microsoft.com/en-us/dotnet/core/versions/ for more information." Write-WhiskeyError -Message $msg return } $desiredVersion = [Version] "$($Matches[1]).$($Matches[2]).$($Matches[3]).$($Matches[4])" $sortedSdkVersions = $sdkVersions | ForEach-Object { $_ -match '^(\d+)\.(\d+)\.(\d{1})(\d+)' | Out-Null [Version] "$($Matches[1]).$($Matches[2]).$($Matches[3]).$($Matches[4])" } | Sort-Object -Descending $resolvedVersion = $null switch ($RollForward) { Disable { $resolvedVersion = $sortedSdkVersions | Where-Object { $_ -eq $desiredVersion } | Select-Object -First 1 } { $_ -eq [WhiskeyDotNetSdkRollForward]::Patch -or $_ -eq [WhiskeyDotNetSdkRollForward]::Feature -or $_ -eq [WhiskeyDotNetSdkRollForward]::Major -or $_ -eq [WhiskeyDotNetSdkRollForward]::Minor -or $_ -eq [WhiskeyDotNetSdkRollForward]::LatestPatch } { $resolvedVersion = $sortedSdkVersions | Where-Object { $_.Major -eq $desiredVersion.Major -and $_.Minor -eq $desiredVersion.Minor -and $_.Build -eq $desiredVersion.Build } | Select-Object -First 1 } LatestFeature { $resolvedVersion = $sortedSdkVersions | Where-Object { $_.Major -eq $desiredVersion.Major -and $_.Minor -eq $desiredVersion.Minor -and $_.Build -ge $desiredVersion.Build } | Select-Object -First 1 } { $_ -eq [WhiskeyDotNetSdkRollForward]::LatestMinor -or $_ -eq [WhiskeyDotNetSdkRollForward]::LatestMajor } { $resolvedVersion = $release | Select-Object -ExpandProperty 'latest-sdk' } Default { $validStrategies = [WhiskeyDotNetSdkRollForward].GetEnumNames() $msg = "Roll forward strategy $($RollForward) is not one of the valid .NET SDK roll forward strategies: " + "$($validStrategies -join ', ')." Write-WhiskeyError -Message $msg return } } if ( -not $resolvedVersion) { Write-WhiskeyError -Message ('A released version of the .NET Core SDK matching "{0}" could not be found in "{1}" with rollForward value in global.json set to {2}' -f $Version, $releasesJsonUri, $RollForward) return } if ( $resolvedVersion -is [Version] ) { $resolvedVersion = '{0}.{1}.{2}{3:00}' -f $resolvedVersion.Major, $resolvedVersion.Minor, $resolvedVersion.Build, $resolvedVersion.Revision } Write-WhiskeyVerbose -Message ('[{0}] SDK version "{1}" resolved to "{2}' -f $MyInvocation.MyCommand, $Version, $resolvedVersion) return $resolvedVersion } function Resolve-WhiskeyNodeModulePath { <# .SYNOPSIS Gets the path to Node module's directory. .DESCRIPTION The `Resolve-WhiskeyNodeModulePath` resolves the path to a Node modules's directory. Pass the name of the module to the `Name` parameter. Pass the path to the build root to the `BuildRootPath` (this is usually where the package.json file is). The function will return the path to the Node module's directory in the local "node_modules" directory. Whiskey installs a private copy of Node for you into a ".node" directory in the build root. If you want to get the path to a global module from this private location, use the `-Global` switch. To get the Node module's directory from an arbitrary directory where Node is installed, pass the install directory to the `NodeRootPath` directory. This function handles the different locations of the "node_modules" directory across/between operating systems. If the Node module isn't installed, you'll get an error and nothing will be returned. .EXAMPLE Resolve-WhiskeyNodeModulePath -Name 'npm' -NodeRootPath $pathToNodeInstallRoot Demonstrates how to get the path to the `npm' module's directory from the "node_modules" directory from a directory where Node is installed, given by the `$pathToInstallRoot` variable. .EXAMPLE Resolve-WhiskeyNodeModulePath -Name 'npm' -BuildRootPath $TaskContext.BuildRoot Demonstrates how to get the path to a Node module's directory where Node installs a local copy. In this case, `Join-Path -Path $TaskContext.BuildRoot -ChildPath 'node_modules\npm'` would be returned (if it exists). .EXAMPLE Resolve-WhiskeyNodeModulePath -Name 'npm' -BuildRootPath $TaskContext.BuildRoot -Global Demonstrates how to get the path to a globally installed Node module's directory. Whiskey installs a private copy of Node into a ".node" directory in the build root, so this example would return a path to the module in that directory (if it exists). That path can be different between operating systems. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The name of the Node module whose path to get. [String]$Name, [Parameter(Mandatory,ParameterSetName='FromBuildRoot')] # The path to the build root. This will return the path to Node modules's directory from the "node_modules" directory in the build root. If you want the path to a global node module, installed in the local Node directory Whiskey installs in the repository, use the `-Global` switch. [String]$BuildRootPath, [Parameter(ParameterSetName='FromBuildRoot')] # Get the path to a Node module in the global "node_modules" directory. The default is to get the path to the copy in the local node_modules directory. [switch]$Global, [Parameter(Mandatory,ParameterSetName='FromNodeRoot')] # The path to the root of a Node package, as downloaded and expanded from the Node.js project. [String]$NodeRootPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $PSCmdlet.ParameterSetName -eq 'FromBuildRoot' ) { if( $Global ) { return (Resolve-WhiskeyNodeModulePath -NodeRootPath (Join-Path -Path $BuildRootPath -ChildPath '.node') -Name $Name) } return (Resolve-WhiskeyNodeModulePath -NodeRootPath $BuildRootPath -Name $Name) } $nodeModulePath = & { Join-Path -Path $NodeRootPath -ChildPath 'lib/node_modules' Join-Path -Path $NodeRootPath -ChildPath 'node_modules' } | ForEach-Object { Join-Path -Path $_ -ChildPath $Name } | Where-Object { Test-Path -Path $_ -PathType Container } | Select-Object -First 1 | Resolve-Path | Select-Object -ExpandProperty 'ProviderPath' if( -not $nodeModulePath ) { Write-WhiskeyError -Message ('Node module "{0}" directory doesn''t exist in "{1}".' -f $Name,$NodeRootPath) -ErrorAction $ErrorActionPreference return } return $nodeModulePath } function Resolve-WhiskeyNodePath { <# .SYNOPSIS Gets the path to the Node executable. .DESCRIPTION The `Resolve-WhiskeyNodePath` resolves the path to the Node executable in a cross-platform manner. The path/name of the Node executable is different on different operating systems. Pass the path to the root directory where Node is installed to the `NodeRootPath` parameter. If you want the path to the local version of Node that Whiskey installs for tasks that need it, pass the build root path to the `BuildRootPath` parameter. Returns the full path to the Node executable. If one isn't found, writes an error and returns nothing. .EXAMPLE Resolve-WhiskeyNodePath -NodeRootPath $pathToNodeInstallRoot Demonstrates how to get the path to the Node executable when the path to the root Node directory is in the `$pathToInstallRoot` variable. .EXAMPLE Resolve-WhiskeyNodePath -BuildRootPath $TaskContext.BuildRoot Demonstrates how to get the path to the Node executable in the directory where Whiskey installs it. #> [CmdletBinding()] param( [Parameter(Mandatory,ParameterSetName='FromBuildRoot')] # The path to the build root. This will return the path to Node where Whiskey installs a local copy. [String]$BuildRootPath, [Parameter(Mandatory,ParameterSetName='FromNodeRoot')] # The path to the root of an Node package, as downloaded and expanded from the Node.js download page. [String]$NodeRootPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $PSCmdlet.ParameterSetName -eq 'FromBuildRoot' ) { return (Resolve-WhiskeyNodePath -NodeRootPath (Join-Path -Path $BuildRootPath -ChildPath '.node')) } $nodePath = & { Join-Path -Path $NodeRootPath -ChildPath 'bin/node' Join-Path -Path $NodeRootPath -ChildPath 'node.exe' } | ForEach-Object { Write-WhiskeyDebug -Message ('Looking for Node executable at "{0}".' -f $_) $_ } | Where-Object { Test-Path -Path $_ -PathType Leaf } | Select-Object -First 1 | Resolve-Path | Select-Object -ExpandProperty 'ProviderPath' if( -not $nodePath ) { Write-WhiskeyError -Message ('Node executable doesn''t exist in "{0}".' -f $NodeRootPath) -ErrorAction $ErrorActionPreference return } Write-WhiskeyDebug -Message ('Found Node executable at "{0}".' -f $nodePath) return $nodePath } function Resolve-WhiskeyRelativePath { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [String] $Path ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $realPath = Resolve-Path -Path $Path -ErrorAction Ignore | Select-Object -ExpandProperty 'ProviderPath' if( $realPath ) { $Path = $realPath } if( -not [IO.Path]::IsPathRooted($Path) ) { $context = Get-WhiskeyContext $Path = Join-Path -Path $Context.BuildRoot.FullName -ChildPath $Path } $currentDir = (Get-Location).Path $currentDir = $currentDir.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) $currentDir = "$($currentDir)$([IO.Path]::DirectorySeparatorChar)" $ignoreCase = $IsWindows if( $Path.StartsWith($currentDir, $ignoreCase, [cultureinfo]::CurrentCulture) ) { $Path = ".$($Path.Substring(($currentDir.Length - 1)))" } return $Path } } function Resolve-WhiskeyTaskPath { <# .SYNOPSIS Resolves paths provided by users to actual paths tasks can use. .DESCRIPTION The `Resolve-WhiskeyTaskPath` function validates and resolves paths provided by users to actual paths. It: * ensures the paths exist (use the `AllowNonexistent` switch to allow paths that don't exist). * ensures the paths exist under the build root (use the `AllowOutsideBuildRoot` switch to allow paths to escape the build root). * can ensure the user provides at least one value (use the `Mandatory` switch). * can ensure the user only provides one path or one path that resolves to a single path (use the `OnlySinglePath` switch). * can ensure that the user provides a path to a file or directory (pass the type you want to the `PathType` parameter). * can create the paths the user passed in (use the `Create` switch, the `AllowNonexistent` switch, and the `PathType` parameters). Wildcards are accepted for all paths and are resolved to actual paths. Paths are resolved relative to the current working directory, which for a Whiskey task is the build root. You must pass the name of the property whose path you're resolving to the `ProperytName` parameter. This is so Whiskey can write friendly error messages to the user. The resolved, relative paths are returned. If paths don't exist, Whiskey will stop and fail the current build. To allow paths to not exist, use the `AllowNonexistent` switch. You can use glob patterns (e.g. `**`) to find files. Pass your patterns to the `Path` parameter and use the `UseGlob` switch. The function installs and uses the [Glob](https://www.powershellgallery.com/packages/Glob) PowerShell module to resolve the patterns to files. .LINK https://www.powershellgallery.com/packages/Glob .EXAMPLE $paths | Resolve-WhiskeyTaskPath -TaskContext $context -PropertyName 'Path' Demonstrates the simplest way to use `Resolve-WhiskeyTaskPath`. .EXAMPLE $paths | Resolve-WhiskeyTaskPath -TaskContext $context -PropertyName 'Path' -Mandatory Demonstrates how to ensure that the user provides at least one path value to resolve. .EXAMPLE $path | Resolve-WhiskeyTaskPath -TaskContext $context -PropertyName 'Path' -OnlySinglePath Demonstrates how to ensure that the path(s) the user provides only resolves to one item. .EXAMPLE $path | Resolve-WhiskeyTaskPath -TaskContext $context -PropertyName 'Path' -PathType 'File' Demonstrates how to ensure that the user has passed paths to only files. .EXAMPLE $path | Resolve-WhiskeyTaskPath -TaskContext $context -PropertyName 'Path' -PathType 'Directory' Demonstrates how to ensure that the user has passed paths to only directories. .EXAMPLE $path | Resolve-WhiskeyTaskPath -TaskContext $context -PropertyName 'Path' -AllowNonexistent Demonstrates how to let the user pass paths to items that may or may not exist. .EXAMPLE $path | Resolve-WhiskeyTaskPath -TaskContext $context -PropertyName 'Path' -Create -AllowNonexistent -PathType File Demonstrates how to get Whiskey to create any non-existent items whose path the user passes. In this example, Whiskey will create files. To create directories, pass `Directory` to the PathType parameter. You *must* use `Create`, `AllowNonexistent`, and `PathType` parameters together. .EXAMPLE $path | Resolve-WhiskeyTaskPath -TaskContext $context -PropertyName 'Path' -AllowOutsideBuildRoot Demonstrates how to allow the user to pass paths to items that are outside the build root. Be very careful using this switch as it could allow attackers to use your task to do nefarious things to servers. #> [CmdletBinding(DefaultParameterSetName='FromParameters')] param( [Parameter(Mandatory)] # An object that holds context about the current build and executing task. [Whiskey.Context]$TaskContext, [Parameter(ValueFromPipeline)] [String]$Path, [Parameter(Mandatory,ParameterSetName='FromAttribute')] # INTERNAL. DO NOT USE. [Management.Automation.ParameterMetadata]$CmdParameter, [Parameter(Mandatory,ParameterSetName='FromAttribute')] # INTERNAL. DO NOT USE. [Whiskey.Tasks.ValidatePathAttribute]$ValidatePathAttribute, [Parameter(Mandatory,ParameterSetName='FromAttribute')] # INTERNAL. DO NOT USE. [hashtable]$TaskParameter, [Parameter(Mandatory,ParameterSetName='FromParameters')] [Parameter(Mandatory,ParameterSetName='FromParametersUsingGlob')] # The name of the property from the user's whiskey.yml file being parsed. Used to output helpful error messages. [String]$PropertyName, [Parameter(ParameterSetName='FromParameters')] # Fail if the path does not resolve to a single path. [switch]$OnlySinglePath, [Parameter(ParameterSetName='FromParameters')] [Parameter(ParameterSetName='FromParametersUsingGlob')] # The `Path` parameter must have at least one value. [switch]$Mandatory, [Parameter(ParameterSetName='FromParameters')] [ValidateSet('File','Directory')] # The type of item the path should be. [String]$PathType, [Parameter(ParameterSetName='FromParameters')] # Allow the paths to not exist. [switch]$AllowNonexistent, [Parameter(ParameterSetName='FromParameters')] # Allow the path to point to something outside the build root. [switch]$AllowOutsideBuildRoot, [Parameter(ParameterSetName='FromParameters')] # Create the path if it doesn't exist. Requires the `PathType` parameter. [switch]$Create, [Parameter(Mandatory,ParameterSetName='FromParametersUsingGlob')] # Whether or not to use glob syntax to find files. Install and uses the [Glob](https://www.powershellgallery.com/packages/Glob) PowerShell module to perform the search. [switch]$UseGlob, [Parameter(ParameterSetName='FromParametersUsingGlob')] # Files to exclude from being returned. [String[]]$Exclude = @() ) begin { Set-StrictMode -Version 'Latest' $pathIdx = -1 if( $PSCmdlet.ParameterSetName -eq 'FromAttribute' ) { $Mandatory = $ValidatePathAttribute.Mandatory if( $ValidatePathAttribute.PathType ) { $PathType = $ValidatePathAttribute.PathType } $AllowNonexistent = $ValidatePathAttribute.AllowNonexistent $AllowOutsideBuildRoot = $ValidatePathAttribute.AllowOutsideBuildRoot $Create = $ValidatePathAttribute.Create $UseGlob = $ValidatePathAttribute.UseGlob if( $ValidatePathAttribute.GlobExcludeParameter ) { $Exclude = $TaskParameter[$ValidatePathAttribute.GlobExcludeParameter] } $PropertyName = $CmdParameter.Name $OnlySinglePath = $CmdParameter.ParameterType -ne [String[]] if( $UseGlob ) { if( $OnlySinglePath ) { Stop-WhiskeyTask -TaskContext $Context -Message ('The "{0}" property is configured to use glob syntax to find matching paths, but the parameter''s type is not [String[]]. This is a task authoring error. If you are the task''s author, please change the "{0}" parameter''s type to be [String[]]. If you are not the task''s author, please contact them to request this change.' -f $PropertyName) return } } } $useGetRelativePath = [IO.Path] | Get-Member -Static -Name 'GetRelativePath' $currentDirRelative = Join-Path -Path '..' -ChildPath (Get-Location | Split-Path -Leaf) $currentDir = (Get-Location).Path $globPaths = [Collections.ArrayList]::new() if( $UseGlob ) { Install-WhiskeyPowerShellModule -Name 'Glob' -Version '0.1.*' -BuildRoot $TaskContext.BuildRoot -ErrorAction Stop | Out-Null } $insideCurrentDirPrefix = '.{0}' -f [IO.Path]::DirectorySeparatorChar $outsideCurrentDirPrefix = '..{0}' -f [IO.Path]::DirectorySeparatorChar # Carbon has a Resolve-RelativePath alias which is why we add a `W` prefix. function Resolve-WRelativePath { param( [Parameter(Mandatory)] [String[]]$Path, [String]$DebugPrefix ) # Now, convert the paths to relative paths. foreach( $resolvedPath in $Path ) { if( $useGetRelativePath ) { $relativePath = [IO.Path]::GetRelativePath($currentDir,$resolvedPath) if( $relativePath -eq '.' ) { $relativePath = '{0}{1}' -f $relativePath,[IO.Path]::DirectorySeparatorChar } } else { if( (Test-Path -Path $resolvedPath) ) { $relativePath = Resolve-Path -Path $resolvedPath -Relative # Resolve-Path likes to resolve the current directory's relative path as ..\DIR_NAME instead of . if( $relativePath -eq $currentDirRelative ) { $relativePath = '.{0}' -f [IO.Path]::DirectorySeparatorChar } } else { # .NET Framework doesn't have a method to convert a non-existent path to a relative path, so we use # P/Invoke to call into Windows shlwapi. $relativePathBuilder = New-Object System.Text.StringBuilder 260 $converted = [Whiskey.Path]::PathRelativePathTo( $relativePathBuilder, $currentDir, [IO.FileAttributes]::Directory, $resolvedPath, [IO.FileAttributes]::Normal ) if( $converted ) { $relativePath = $relativePathBuilder.ToString() } else { $relativePath = $resolvedPath } } } # Files/directories that begin with a period don't get the .\ or ./ prefix put on them. if( -not $relativePath.StartsWith($insideCurrentDirPrefix) -and -not $relativePath.StartsWith($outsideCurrentDirPrefix) ) { $relativePath = Join-Path -Path '.' -ChildPath $relativePath } if( $DebugPrefix ) { Write-WhiskeyDebug -Context $TaskContext -Message ('{0} -> {1}' -f $DebugPrefix,$relativePath) } Write-Output $relativePath } } } process { Set-StrictMode -Version 'Latest' $pathIdx++ if( -not $Path ) { if( $Mandatory ) { Stop-WhiskeyTask -TaskContext $Context ` -PropertyName $PropertyName ` -Message ('{0} is mandatory.' -f $PropertyName) return } return } $result = $Path $resolvedPaths = $null # Normalize the directory separators, otherwise, if a path begins with '\', on Linux (and probably macOS), # `IsPathRooted` doesn't think the path is rooted. $normalizedPath = $result | Convert-WhiskeyPathDirectorySeparator if( $UseGlob ) { if( [IO.Path]::IsPathRooted($normalizedPath) ) { $normalizedPath = Resolve-WRelativePath -Path $normalizedPath if( $normalizedPath.StartsWith($insideCurrentDirPrefix) -and -not $normalizedPath.StartsWith($outsideCurrentDirPrefix) ) { $normalizedPath = $normalizedPath.Substring(2) } } [void]$globPaths.Add($normalizedPath) } else { $message = 'Resolve {0} ->' -f $Path $prefix = ' ' * ($message.Length - 3) Write-WhiskeyDebug -Context $TaskContext -Message $message if( -not [IO.Path]::IsPathRooted($normalizedPath) ) { # Get the full path to the item $normalizedPath = Join-Path -Path $currentDir -ChildPath $result } # Remove all the '..' and '.' path parts from the path. if( -not [wildcardpattern]::ContainsWildcardCharacters($normalizedPath) ) { $normalizedPath = [IO.Path]::GetFullPath($normalizedPath) } if( (Test-Path -Path $normalizedPath) ) { $resolvedPaths = Get-Item -Path $normalizedPath -Force | Select-Object -ExpandProperty 'FullName' } if( -not $resolvedPaths ) { if( -not $AllowNonexistent ) { Stop-WhiskeyTask -TaskContext $TaskContext ` -Message ('{0}[{1}] "{2}" does not exist.' -f $PropertyName,$pathIdx,$Path) return } $resolvedPaths = $normalizedPath # If it contains a wildcard, it didn't resolve to anything, so don't return it. if( [wildcardpattern]::ContainsWildcardCharacters($resolvedPaths) ) { return } } if( -not $AllowOutsideBuildRoot ) { $fsCaseSensitive = -not (Test-Path -Path ($TaskContext.BuildRoot.FullName.ToUpperInvariant())) $comparer = [System.StringComparison]::OrdinalIgnoreCase if( $fsCaseSensitive ) { $comparer = [System.StringComparison]::Ordinal } $normalizedBuildRoot = $TaskContext.BuildRoot.FullName.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) $normalizedBuildRoot = '{0}{1}' -f $normalizedBuildRoot,[IO.Path]::DirectorySeparatorChar $invalidPaths = $resolvedPaths | Where-Object { -not ( $_.StartsWith($normalizedBuildRoot, $comparer) ) } | # What if the user supplies '.' for the current directory? Where-Object { ('{0}{1}' -f $_,[IO.Path]::DirectorySeparatorChar) -ne $normalizedBuildRoot } if( $invalidPaths ) { Stop-WhiskeyTask -TaskContext $TaskContext ` -Message ('{0}[{1}] "{2}" is outside the build root "{3}".' -f $PropertyName,$pathIdx,$Path,$TaskContext.BuildRoot) return } } $expectedPathType = $PathType if( $expectedPathType -and -not $AllowNonexistent ) { $itemType = 'Leaf' if( $expectedPathType -eq 'Directory' ) { $itemType = 'Container' } $invalidPaths = $resolvedPaths | Where-Object { -not (Test-Path -Path $_ -PathType $itemType) } if( $invalidPaths ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName $PropertyName -Message (@' Found {0} paths that should resolve to a {1}, but don''t: * {2} '@ -f ($invalidPaths | Measure-Object).Count,$expectedPathType.ToLowerInvariant(),($invalidPaths -join ('{0}* ' -f [Environment]::NewLine))) return } } $pathCount = $resolvedPaths | Measure-Object | Select-Object -ExpandProperty 'Count' if( $OnlySinglePath -and $pathCount -gt 1 ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName $CmdParameter.Name -Message (@' The value "{1}" resolved to {2} paths [1] but this task requires a single path. Please change "{1}" to a value that resolves to a single item. If you are this task''s author, and you want this property to accept multiple paths, please update the "{3}" command''s "{0}" property so it''s type is "[String[]]". [1] The {1} path resolved to: * {4} '@ -f $CmdParameter.Name,$Path,$pathCount,$TaskContext.TaskName,($resolvedPaths -join ('{0}* ' -f [Environment]::NewLine))) } if( $Create ) { if( -not $PathType ) { Write-WhiskeyError -Message ('The ValidatePath attribute on the "{0}" task''s "{1}" property has Create set to true but the attribute doesn''t specify a value for the PathType property. This is a task authoring error. The task''s author must update this ValidatePath attribute to either remove its Create property (so Whiskey doesn''t try to create non-existent items) or add a PathType property and set its value to either "File" or "Directory" (so Whiskey knows what kind of item to create).' -f $TaskContext.TaskName,$CmdParameter.Name) -ErrorAction Stop return } foreach( $item in $resolvedPaths ) { if( (Test-Path -Path $item) ) { continue } New-Item -Path $item -ItemType $PathType -Force | Out-Null } } } if( $resolvedPaths ) { Resolve-WRelativePath -Path $resolvedPaths -DebugPrefix $prefix } } end { if( -not $UseGlob ) { return } $globPathsStats = $globPaths | Measure-Object -Maximum -Property 'Length' $longestPathLength = $globPathsStats.Maximum $messageFormat = 'Resolve {{0,-{0}}} ->' -f $longestPathLength $message = $messageFormat -f ($globPaths | Select-Object -First 1) $prefix = ' ' * ($message.Length - 3) Write-WhiskeyDebug -Context $TaskContext -Message $message $messageFormat = $messageFormat -replace '^Resolve',' ' foreach( $globPath in ($globPaths | Select-Object -Skip 1) ) { Write-WhiskeyDebug -Context $TaskContext -Message ($messageFormat -f $globPath) } # Detect the case-sensitivity of the current directory so we can do a case-sensitive search if current directory # is on a case-sensitive file system. $parentPath = '' # Split-Path throws an exception if passed / in PowerShell Core. if( $currentDir -ne [IO.Path]::DirectorySeparatorChar -and $currentDir -ne [IO.Path]::AltDirectorySeparatorChar ) { $parentPath = Split-Path -Path $currentDir -ErrorAction Ignore } $childName = Split-Path -Leaf -Path $currentDir # If we're in the root of the file system. if( -not $parentPath -or -not $childName ) { $childPath = Get-ChildItem -Path $currentDir | Select-Object -First 1 | Select-Object -ExpandProperty 'FullName' $parentPath = Split-Path -Path $childPath if( -not $parentPath ) { $parentPath = [IO.Path]::DirectorySeparatorChar } $childName = Split-Path -Leaf -Path $childPath } $caseSensitivePath = [Text.StringBuilder]::New((Join-Path -Path $parentPath -ChildPath $childName)) for( $idx = $caseSensitivePath.Length - 1; $idx -ge 0; --$idx ) { $char = $caseSensitivePath[$idx] $isUpper = [char]::IsUpper($char) $isLower = [char]::IsLower($char) if( -not ($isUpper -or $isLower) ) { # Not a character so move on to the next. continue } if( $isUpper ) { $caseSensitivePath[$idx] = [char]::ToLower($char) break } $caseSensitivePath[$idx] = [char]::ToUpper($char) } $caseSensitive = -not (Test-Path -Path $caseSensitivePath.ToString()) # We only want to hit the file system once, since globs are pretty greedy. $resolvedPaths = Find-GlobFile -Path $currentDir -Include $globPaths -Exclude $Exclude -Force -CaseSensitive:$caseSensitive | Select-Object -ExpandProperty 'FullName' if( -not $resolvedPaths ) { if( -not $AllowNonexistent ) { $pluralSuffix = '' if( $globPathsStats.Count -gt 1 ) { $pluralSuffix = '(s)' } $exclusionFilters = '' if( $Exclude ) { $exclusionFilters = ' (and excluding "{0}")' -f ($Exclude -join ', ') } Stop-WhiskeyTask -TaskContext $TaskContext ` -Message ('{0}: glob pattern{1} "{2}"{3} did not match any files.' -f $PropertyName,$pluralSuffix,($globPaths -join ', '),$exclusionFilters) } return } Resolve-WRelativePath -Path $resolvedPaths -DebugPrefix $prefix } } function Resolve-WhiskeyTaskPathInternal { [CmdletBinding()] param( [Parameter(Mandatory)] # An object that holds context about the current build and executing task. [Whiskey.Context]$TaskContext, [Parameter(Mandatory,ValueFromPipeline)] [String]$Path, [Parameter(Mandatory)] [String]$PropertyName, # The root directory to use when resolving paths. The default is to use the `$TaskContext.BuildRoot` directory. Each path must be relative to this path. [String]$ParentPath, # Create the path if it doesn't exist. By default, the path will be created as a directory. To create the path as a file, pass `File` to the `PathType` parameter. [switch]$Force, [ValidateSet('Directory','File')] # The type of item to create when using the `Force` parameter to create paths that don't exist. The default is to create the path as a directory. Pass `File` to create the path as a file. [String]$PathType = 'Directory' ) begin { Set-StrictMode -Version 'Latest' $pathIdx = -1 } process { Set-StrictMode -Version 'Latest' $pathIdx++ $originalPath = $Path if( [IO.Path]::IsPathRooted($Path) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('{0}[{1}] ''{2}'' is absolute but must be relative to the ''{3}'' file.' -f $PropertyName,$pathIdx,$Path,$TaskContext.ConfigurationPath) return } if( -not $ParentPath ) { $ParentPath = $TaskContext.BuildRoot } $Path = Join-Path -Path $ParentPath -ChildPath $Path if( -not (Test-Path -Path $Path) ) { if( $Force ) { New-Item -Path $Path -ItemType $PathType -Force | Out-String | Write-WhiskeyDebug -Context $TaskContext } else { if( $ErrorActionPreference -ne [Management.Automation.ActionPreference]::Ignore ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('{0}[{1}] "{2}" does not exist.' -f $PropertyName,$pathIdx,$Path) } return } } $message = 'Resolve {0} ->' -f $originalPath $prefix = ' ' * ($message.Length - 3) Write-WhiskeyDebug -Context $TaskContext -Message $message Resolve-Path -Path $Path | Select-Object -ExpandProperty 'ProviderPath' | ForEach-Object { Write-WhiskeyDebug -Context $TaskContext -Message ('{0} -> {1}' -f $prefix,$_) $_ } } end { } } function Resolve-WhiskeyVariable { <# .SYNOPSIS Replaces any variables in a string to their values. .DESCRIPTION The `Resolve-WhiskeyVariable` function replaces any variables in strings, arrays, or hashtables with their values. Variables have the format `$(VARIABLE_NAME)`. Variables are expanded in each item of an array. Variables are expanded in each value of a hashtable. If an array or hashtable contains an array or hashtable, variables are expanded in those objects as well, i.e. `Resolve-WhiskeyVariable` recursivelye expands variables in all arrays and hashtables. You can add variables to replace via the `Add-WhiskeyVariable` function. If a variable doesn't exist, environment variables are used. If a variable has the same name as an environment variable, the variable value is used instead of the environment variable's value. If no variable or environment variable is found, `Resolve-WhiskeyVariable` will write an error and return the origin string. See the [Variables](https://github.com/webmd-health-services/Whiskey/wiki/Variables) page on the [Whiskey wiki](https://github.com/webmd-health-services/Whiskey/wiki) for a list of variables. .EXAMPLE '$(COMPUTERNAME)' | Resolve-WhiskeyVariable Demonstrates that you can use environment variable as variables. In this case, `Resolve-WhiskeyVariable` would return the name of the current computer. .EXAMPLE @( '$(VARIABLE)', 4, @{ 'Key' = '$(VARIABLE') } ) | Resolve-WhiskeyVariable Demonstrates how to replace all the variables in an array. Any value of the array that isn't a string is ignored. Any hashtable in the array will have any variables in its values replaced. In this example, if the value of `VARIABLE` is 'Whiskey`, `Resolve-WhiskeyVariable` would return: @( 'Whiskey', 4, @{ Key = 'Whiskey' } ) .EXAMPLE @{ 'Key' = '$(Variable)'; 'Array' = @( '$(VARIABLE)', 4 ) 'Integer' = 4; } | Resolve-WhiskeyVariable Demonstrates that `Resolve-WhiskeyVariable` searches hashtable values and replaces any variables in any strings it finds. If the value of `VARIABLE` is set to `Whiskey`, then the code in this example would return: @{ 'Key' = 'Whiskey'; 'Array' = @( 'Whiskey', 4 ); 'Integer' = 4; } #> [CmdletBinding(DefaultParameterSetName='ByPipeline')] param( [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ByPipeline')] [AllowNull()] # The object on which to perform variable replacement/substitution. If the value is a string, all variables in the string are replaced with their values. # # If the value is an array, variable expansion is done on each item in the array. # # If the value is a hashtable, variable replcement is done on each value of the hashtable. # # Variable expansion is performed on any arrays and hashtables found in other arrays and hashtables, i.e. arrays and hashtables are searched recursively. [Object]$InputObject, [Parameter(ParameterSetName='ByName')] # The name of a single Whiskey variable to resolve. [String]$Name, [Parameter(Mandatory)] # The context of the current build. Necessary to lookup any variables. [Whiskey.Context]$Context ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $Name ) { $InputObject = '$({0})' -f $Name } $version = $Context.Version $prereleaseID = '' if( $version.SemVer2.Prerelease -match '^(.*)\..*$' ) { $prereleaseID = $Matches[1] } $buildInfo = $Context.BuildMetadata $sem1Version = '' if( $version.SemVer1 ) { $sem1Version = '{0}.{1}.{2}' -f $version.SemVer1.Major,$version.SemVer1.Minor,$version.SemVer1.Patch } $sem2Version = '' if( $version.SemVer2 ) { $sem2Version = '{0}.{1}.{2}' -f $version.SemVer2.Major,$version.SemVer2.Minor,$version.SemVer2.Patch } $wellKnownVariables = @{ 'WHISKEY_BUILD_ID' = $buildInfo.BuildID; 'WHISKEY_BUILD_NUMBER' = $buildInfo.BuildNumber; 'WHISKEY_BUILD_ROOT' = $Context.BuildRoot; 'WHISKEY_BUILD_SERVER_NAME' = $buildInfo.BuildServer; 'WHISKEY_BUILD_STARTED_AT' = $Context.StartedAt; 'WHISKEY_BUILD_URI' = $buildInfo.BuildUri; 'WHISKEY_ENVIRONMENT' = $Context.Environment; 'WHISKEY_JOB_URI' = $buildInfo.JobUri; 'WHISKEY_MSBUILD_CONFIGURATION' = (Get-WhiskeyMSBuildConfiguration -Context $Context); 'WHISKEY_OUTPUT_DIRECTORY' = $Context.OutputDirectory; 'WHISKEY_PIPELINE_NAME' = $Context.PipelineName; 'WHISKEY_SCM_BRANCH' = $buildInfo.ScmBranch; 'WHISKEY_SCM_COMMIT_ID' = $buildInfo.ScmCommitID; 'WHISKEY_SCM_URI' = $buildInfo.ScmUri; 'WHISKEY_SEMVER1' = $version.SemVer1; 'WHISKEY_SEMVER1_VERSION' = $sem1Version; 'WHISKEY_SEMVER2' = $version.SemVer2; 'WHISKEY_SEMVER2_NO_BUILD_METADATA' = $version.SemVer2NoBuildMetadata; 'WHISKEY_SEMVER2_PRERELEASE_ID' = $prereleaseID 'WHISKEY_SEMVER2_VERSION' = $sem2Version; 'WHISKEY_TASK_NAME' = $Context.TaskName; 'WHISKEY_TEMP_DIRECTORY' = (Get-Item -Path ([IO.Path]::GetTempPath())); 'WHISKEY_TASK_TEMP_DIRECTORY' = $Context.Temp; 'WHISKEY_VERSION' = $version.Version; } } process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $null -eq $InputObject -or $InputObject -is [scriptblock] ) { return $InputObject } if( (Get-Member -Name 'Keys' -InputObject $InputObject) ) { $newValues = @{ } $toRemove = New-Object 'Collections.Generic.List[String]' # Can't modify a collection while enumerating it. foreach( $key in $InputObject.Keys ) { $newKey = $key | Resolve-WhiskeyVariable -Context $Context if( $newKey -ne $key ) { $toRemove.Add($key) } $newValues[$newKey] = Resolve-WhiskeyVariable -Context $Context -InputObject $InputObject[$key] } foreach( $key in $newValues.Keys ) { $InputObject[$key] = $newValues[$key] } $toRemove | ForEach-Object { $InputObject.Remove($_) } | Out-Null return $InputObject } if( (Get-Member -Name 'Count' -InputObject $InputObject) ) { for( $idx = 0; $idx -lt $InputObject.Count; ++$idx ) { $InputObject[$idx] = Resolve-WhiskeyVariable -Context $Context -InputObject $InputObject[$idx] } return ,$InputObject } $startAt = 0 $haystack = $InputObject.ToString() do { # Parse the variable expression, everything between $( and ) $needleStart = $haystack.IndexOf('$(',$startAt) if( $needleStart -lt 0 ) { break } elseif( $needleStart -gt 0 ) { if( $haystack[$needleStart - 1] -eq '$' ) { $haystack = $haystack.Remove($needleStart - 1, 1) $startAt = $needleStart continue } } # Variable expressions can contain method calls, which begin and end with parenthesis, so # make sure you don't treat the close parenthesis of a method call as the close parenthesis # to the current variable expression. $needleEnd = $needleStart + 2 $depth = 0 while( $needleEnd -lt $haystack.Length ) { $currentChar = $haystack[$needleEnd] if( $currentChar -eq ')' ) { if( $depth -eq 0 ) { break } $depth-- } elseif( $currentChar -eq '(' ) { $depth++ } ++$needleEnd } $variableName = $haystack.Substring($needleStart + 2, $needleEnd - $needleStart - 2) $memberName = $null $arguments = $null # Does the variable expression contain a method call? if( $variableName -match '([^.]+)\.([^.(]+)(\(([^)]+)\))?' ) { $variableName = $Matches[1] $memberName = $Matches[2] $arguments = $Matches[4] $arguments = & { if( -not $arguments ) { return } $currentArg = New-Object 'Text.StringBuilder' $currentChar = $null $inString = $false # Parse each of the arguments in the method call. Each argument is # seperated by a comma. Ignore whitespace. Commas and whitespace that # are part of an argument must be double or single quoted. To include # a double quote inside a double-quoted string, double it. To include # a single quote inside a single-quoted string, double it. for( $idx = 0; $idx -lt $arguments.Length; ++$idx ) { $nextChar = '' if( ($idx + 1) -lt $arguments.Length ) { $nextChar = $arguments[$idx + 1] } $currentChar = $arguments[$idx] if( $currentChar -eq '"' -or $currentChar -eq "'" ) { if( $inString ) { if( $nextChar -eq $currentChar ) { [Void]$currentArg.Append($currentChar) $idx++ continue } } $inString = -not $inString continue } if( $currentChar -eq ',' -and -not $inString ) { $currentArg.ToString() [Void]$currentArg.Clear() continue } if( $inString -or -not [String]::IsNullOrWhiteSpace($currentChar) ) { [Void]$currentArg.Append($currentChar) } } if( $currentArg.Length ) { $currentArg.ToString() } } } $envVarPath = 'env:{0}' -f $variableName if( $Context.Variables.ContainsKey($variableName) ) { $value = $Context.Variables[$variableName] } elseif( $wellKnownVariables.ContainsKey($variableName) ) { $value = $wellKnownVariables[$variableName] } elseif( [Environment] | Get-Member -Static -Name $variableName ) { $value = [Environment]::$variableName } elseif( (Test-Path -Path $envVarPath) ) { $value = (Get-Item -Path $envVarPath).Value } else { Write-WhiskeyError -Context $Context -Message ('Variable ''{0}'' does not exist. We were trying to replace it in the string ''{1}''. You can: * Use the `Add-WhiskeyVariable` function to add a variable named ''{0}'', e.g. Add-WhiskeyVariable -Context $context -Name ''{0}'' -Value VALUE. * Create an environment variable named ''{0}''. * Prevent variable expansion by escaping the variable with a backtick or backslash, e.g. `$({0}) or \$({0}). * Remove the variable from the string. ' -f $variableName,$InputObject) -ErrorAction $ErrorActionPreference return $InputObject } if( $value -eq $null ) { $value = '' } if( $value -ne $null -and $memberName ) { if( -not (Get-Member -Name $memberName -InputObject $value ) ) { Write-WhiskeyError -Context $Context -Message ('Variable ''{0}'' does not have a ''{1}'' member. Here are the available members:{2} {2}{3}{2} ' -f $variableName,$memberName,[Environment]::NewLine,($value | Get-Member | Out-String)) return $InputObject } if( $arguments ) { try { $value = $value.$memberName.Invoke($arguments) } catch { Write-WhiskeyError -Context $Context -Message ('Failed to call ([{0}]{1}).{2}(''{3}''): {4}.' -f $value.GetType().FullName,$value,$memberName,($arguments -join ''','''),$_) return $InputObject } } else { $value = $value.$memberName } } $variableNumChars = $needleEnd - $needleStart + 1 if( $needleStart + $variableNumChars -gt $haystack.Length ) { Write-WhiskeyError -Context $Context -Message ('Unclosed variable expression ''{0}'' in value ''{1}''. Add a '')'' to the end of this value or escape the variable expression with a double dollar sign, e.g. ''${1}''.' -f $haystack.Substring($needleStart),$haystack) return $InputObject } $haystack = $haystack.Remove($needleStart,$variableNumChars) $haystack = $haystack.Insert($needleStart,$value) # No need to keep searching where we've already looked. $startAt = $needleStart } while( $true ) return $haystack } } function Set-WhiskeyBuildStatus { [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$Context, [Parameter(Mandatory)] [ValidateSet('Started','Completed','Failed')] # The build status. Should be one of `Started`, `Completed`, or `Failed`. [String]$Status ) Set-StrictMode -Version 'Latest' if( $Context.ByDeveloper ) { return } $reportingTo = $Context.Configuration['PublishBuildStatusTo'] if( -not $reportingTo ) { return } $reporterIdx = -1 foreach( $reporter in $reportingTo ) { $reporterIdx++ $reporterName = $reporter.Keys | Select-Object -First 1 $propertyDescription = 'PublishBuildStatusTo[{0}]: {1}' -f $reporterIdx,$reporterName $reporterConfig = $reporter[$reporterName] switch( $reporterName ) { 'BitbucketServer' { Install-WhiskeyPowerShellModule -Name 'BitbucketServerAutomation' -Version '0.9.*' -BuildRoot $Context.BuildRoot $uri = $reporterConfig['Uri'] if( -not $uri ) { Stop-WhiskeyTask -TaskContext $Context -PropertyDescription $propertyDescription -Message (@' Property 'Uri' does not exist or does not have a value. Set this property to the Bitbucket Server URI where you want build statuses reported to, e.g., PublishBuildStatusTo: - BitbucketServer: Uri: BITBUCKET_SERVER_URI CredentialID: CREDENTIAL_ID '@ -f $uri) return } $credID = $reporterConfig['CredentialID'] if( -not $credID ) { Stop-WhiskeyTask -TaskContext $Context -PropertyDescription $propertyDescription -Message (@' Property 'CredentialID' does not exist or does not have a value. Set this property to the ID of the credential to use when connecting to the Bitbucket Server at '{0}', e.g., PublishBuildStatusTo: - BitbucketServer: Uri: {0} CredentialID: CREDENTIAL_ID Use the `Add-WhiskeyCredential` function to add the credential to the build.` '@ -f $uri) return } $credential = Get-WhiskeyCredential -Context $Context -ID $credID -PropertyName 'CredentialID' -PropertyDescription $propertyDescription $conn = New-BBServerConnection -Credential $credential -Uri $uri $statusMap = @{ 'Started' = 'INPROGRESS'; 'Completed' = 'Successful'; 'Failed' = 'Failed' } $buildInfo = $Context.BuildMetadata Set-BBServerCommitBuildStatus -Connection $conn -Status $statusMap[$Status] -CommitID $buildInfo.ScmCommitID -Key $buildInfo.JobUri -BuildUri $buildInfo.BuildUri -Name $buildInfo.JobName } default { Stop-WhiskeyTask -TaskContext $Context -PropertyDescription $propertyDescription -Message ('Unknown build status reporter ''{0}''. Supported reporters are ''BitbucketServer''.' -f $reporterName) return } } } } function Set-WhiskeyMSBuildConfiguration { <# .SYNOPSIS Changes the configuration to use when running any MSBuild-based task/tool. .DESCRIPTION The `Set-WhiskeyMSBuildConfiguration` function sets the configuration to use when running any MSBuild-based task/tool (e.g. the `MSBuild`, `DotNetBuild`, `DotNetPublish`, etc.). Usually, the value should be set to either `Debug` or `Release`. Use `Get-WhiskeyMSBuildConfiguration` to get the current configuration. .EXAMPLE Set-WhiskeyMSBuildConfiguration -Context $Context -Value 'Release' Demonstrates how to set the configuration to use when running MSBuild tasks/tools. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The context of the build whose MSBuild configuration you want to set. Use `New-WhiskeyContext` to create a context. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The configuration to use. [String]$Value ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $Context.MSBuildConfiguration = $Value } function Stop-Whiskey { [CmdletBinding()] param( [Parameter(Mandatory)] # An object [Whiskey.Context]$Context, [Parameter(Mandatory)] [String]$Message ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState throw '{0}: {1}' -f $Context.ConfigurationPath,$Message } function Stop-WhiskeyTask { <# .SYNOPSIS Fails a Whiskey build by writing a terminating exception. .DESCRIPTION The `Stop-WhiskeyTask` function fails the current task and build by writing a terminating exception. Pass the current task's context to the `TaskContext` parameter. Pass a failure message to the `Message` property. Whiskey will fail the build with an error message that explans what task in what whiskey.yml file failed. If your build is failing because a task property is invalid, pass the name of the property to the `PropertyName` parameter. The property's name will be inserted into the error message. If you want to customize the task description in the error message, pass that description to the `PropertyDescription` parameter. Instead of using 'Task "TASK_NAME"' in the error message, Whiskey will use the value of the PropertyDescription parameter. .EXAMPLE Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Something bad happened!' Demonstrates how to fail and stop the current build with the message "Something bad happened!". .EXAMPLE Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Must be a number.' -PropertyName 'Count' Demonstrates how to add the name of an invalid property to the error message. The result of this example will be to have an error message like 'whiskey.yml: Task "MyTask": Property "Count": Must be a number.' .EXAMPLE Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Bad things!' -PropertyDescription '"Fubar" task's "Snafu" property' Demonstrates how to customize the task name portion of the error message. In this case, Whiskey will write an error message like 'whiskey.yml: "Fubar" task's "Snafu" property: Bad things!'. #> [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [String]$Message, [String]$PropertyName, [String]$PropertyDescription ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not ($PropertyDescription) ) { $PropertyDescription = 'Task "{0}"' -f $TaskContext.TaskName } if( $PropertyName ) { $PropertyName = ': Property "{0}"' -f $PropertyName } if( $ErrorActionPreference -ne 'Ignore' ) { $message = '{0}: {1}{2}: {3}' -f $TaskContext.ConfigurationPath,$PropertyDescription,$PropertyName,$Message Write-WhiskeyError -Context $TaskContext -Message $message -ErrorAction Stop } } function Test-WhiskeyTaskSkip { <# .SYNOPSIS Determines if the current Whiskey task should be skipped. .DESCRIPTION The `Test-WhiskeyTaskSkip` function returns `$true` or `$false` indicating whether the current Whiskey task should be skipped. It determines if the task should be skipped by comparing values in the Whiskey context and common task properties. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The context for the build. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The common task properties defined for the current task. [hashtable]$Properties ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $Properties['OnlyBy'] -and $Properties['ExceptBy'] ) { Stop-WhiskeyTask -TaskContext $Context -Message ('This task defines both "OnlyBy" and "ExceptBy" properties. Only one of these can be used. Please remove one or both of these properties and re-run your build.') return } elseif( $Properties['OnlyBy'] ) { [Whiskey.RunBy]$onlyBy = [Whiskey.RunBy]::Developer if( -not ([Enum]::TryParse($Properties['OnlyBy'], [ref]$onlyBy)) ) { Stop-WhiskeyTask -TaskContext $Context -PropertyName 'OnlyBy' -Message ('invalid value: ''{0}''. Valid values are ''{1}''.' -f $Properties['OnlyBy'],([Enum]::GetValues([Whiskey.RunBy]) -join ''', ''')) return } if( $onlyBy -ne $Context.RunBy ) { Write-WhiskeyVerbose -Context $Context -Message ('OnlyBy.{0} -ne Build.RunBy.{1}' -f $onlyBy,$Context.RunBy) return $true } } elseif( $Properties['ExceptBy'] ) { [Whiskey.RunBy]$exceptBy = [Whiskey.RunBy]::Developer if( -not ([Enum]::TryParse($Properties['ExceptBy'], [ref]$exceptBy)) ) { Stop-WhiskeyTask -TaskContext $Context -PropertyName 'ExceptBy' -Message ('invalid value: ''{0}''. Valid values are ''{1}''.' -f $Properties['ExceptBy'],([Enum]::GetValues([Whiskey.RunBy]) -join ''', ''')) return } if( $exceptBy -eq $Context.RunBy ) { Write-WhiskeyVerbose -Context $Context -Message ('ExceptBy.{0} -eq Build.RunBy.{1}' -f $exceptBy,$Context.RunBy) return $true } } $branch = $Context.BuildMetadata.ScmBranch if( $Properties['OnlyOnBranch'] -and $Properties['ExceptOnBranch'] ) { Stop-WhiskeyTask -TaskContext $Context -Message ('This task defines both OnlyOnBranch and ExceptOnBranch properties. Only one of these can be used. Please remove one or both of these properties and re-run your build.') return } if( $Properties['OnlyOnBranch'] ) { $runTask = $false Write-WhiskeyVerbose -Context $Context -Message ('OnlyOnBranch') foreach( $wildcard in $Properties['OnlyOnBranch'] ) { if( $branch -like $wildcard ) { $runTask = $true Write-WhiskeyVerbose -Context $Context -Message (' {0} -like {1}' -f $branch, $wildcard) break } Write-WhiskeyVerbose -Context $Context -Message (' {0} -notlike {1}' -f $branch, $wildcard) } if( -not $runTask ) { return $true } } if( $Properties['ExceptOnBranch'] ) { $runTask = $true Write-WhiskeyVerbose -Context $Context -Message ('ExceptOnBranch') foreach( $wildcard in $Properties['ExceptOnBranch'] ) { if( $branch -like $wildcard ) { $runTask = $false Write-WhiskeyVerbose -Context $Context -Message (' {0} -like {1}' -f $branch, $wildcard) break } Write-WhiskeyVerbose -Context $Context -Message (' {0} -notlike {1}' -f $branch, $wildcard) } if( -not $runTask ) { return $true } } $modes = @( 'Clean', 'Initialize', 'Build' ) $onlyDuring = $Properties['OnlyDuring'] $exceptDuring = $Properties['ExceptDuring'] if ($onlyDuring -and $exceptDuring) { Stop-WhiskeyTask -TaskContext $Context -Message 'Both ''OnlyDuring'' and ''ExceptDuring'' properties are used. These properties are mutually exclusive, i.e. you may only specify one or the other.' return } elseif ($onlyDuring -and ($onlyDuring -notin $modes)) { Stop-WhiskeyTask -TaskContext $Context -Message ('Property ''OnlyDuring'' has an invalid value: ''{0}''. Valid values are: ''{1}''.' -f $onlyDuring,($modes -join "', '")) return } elseif ($exceptDuring -and ($exceptDuring -notin $modes)) { Stop-WhiskeyTask -TaskContext $Context -Message ('Property ''ExceptDuring'' has an invalid value: ''{0}''. Valid values are: ''{1}''.' -f $exceptDuring,($modes -join "', '")) return } if ($onlyDuring -and ($Context.RunMode -ne $onlyDuring)) { Write-WhiskeyVerbose -Context $Context -Message ('OnlyDuring.{0} -ne Build.RunMode.{1}' -f $onlyDuring,$Context.RunMode) return $true } elseif ($exceptDuring -and ($Context.RunMode -eq $exceptDuring)) { Write-WhiskeyVerbose -Context $Context -Message ('ExceptDuring.{0} -ne Build.RunMode.{1}' -f $exceptDuring,$Context.RunMode) return $true } if( $Properties['IfExists'] ) { $exists = Test-Path -Path $Properties['IfExists'] if( -not $exists ) { Write-WhiskeyVerbose -Context $Context -Message ('IfExists {0} not exists' -f $Properties['IfExists']) return $true } Write-WhiskeyVerbose -Context $Context -Message ('IfExists {0} exists' -f $Properties['IfExists']) } if( $Properties['UnlessExists'] ) { $exists = Test-Path -Path $Properties['UnlessExists'] if( $exists ) { Write-WhiskeyVerbose -Context $Context -Message ('UnlessExists {0} exists' -f $Properties['UnlessExists']) return $true } Write-WhiskeyVerbose -Context $Context -Message ('UnlessExists {0} not exists' -f $Properties['UnlessExists']) } if( $Properties['OnlyIfBuild'] ) { [Whiskey.BuildStatus]$buildStatus = [Whiskey.BuildStatus]::Succeeded if( -not ([Enum]::TryParse($Properties['OnlyIfBuild'], [ref]$buildStatus)) ) { Stop-WhiskeyTask -TaskContext $Context -PropertyName 'OnlyIfBuild' -Message ('invalid value: ''{0}''. Valid values are ''{1}''.' -f $Properties['OnlyIfBuild'],([Enum]::GetValues([Whiskey.BuildStatus]) -join ''', ''')) return } if( $buildStatus -ne $Context.BuildStatus ) { Write-WhiskeyVerbose -Context $Context -Message ('OnlyIfBuild.{0} -ne Build.BuildStatus.{1}' -f $buildStatus,$Context.BuildStatus) return $true } } if( $Properties['OnlyOnPlatform'] ) { $shouldSkip = $true [Whiskey.Platform]$platform = [Whiskey.Platform]::Unknown foreach( $item in $Properties['OnlyOnPlatform'] ) { if( -not [Enum]::TryParse($item,[ref]$platform) ) { $validValues = [Enum]::GetValues([Whiskey.Platform]) | Where-Object { $_ -notin @( 'Unknown', 'All' ) } Stop-WhiskeyTask -TaskContext $Context -PropertyName 'OnlyOnPlatform' -Message ('Invalid platform "{0}". Valid values are "{1}".' -f $item,($validValues -join '", "')) return } $platform = [Whiskey.Platform]$item if( $CurrentPlatform.HasFlag($platform) ) { Write-WhiskeyVerbose -Context $Context -Message ('OnlyOnPlatform {0} -eq {1}' -f $platform,$CurrentPlatform) $shouldSkip = $false break } else { Write-WhiskeyVerbose -Context $Context -Message ('OnlyOnPlatform ! {0} -ne {1}' -f $platform,$CurrentPlatform) } } return $shouldSkip } if( $Properties['ExceptOnPlatform'] ) { $shouldSkip = $false [Whiskey.Platform]$platform = [Whiskey.Platform]::Unknown foreach( $item in $Properties['ExceptOnPlatform'] ) { if( -not [Enum]::TryParse($item,[ref]$platform) ) { $validValues = [Enum]::GetValues([Whiskey.Platform]) | Where-Object { $_ -notin @( 'Unknown', 'All' ) } Stop-WhiskeyTask -TaskContext $Context -PropertyName 'ExceptOnPlatform' -Message ('Invalid platform "{0}". Valid values are "{1}".' -f $item,($validValues -join '", "')) return } $platform = [Whiskey.Platform]$item if( $CurrentPlatform.HasFlag($platform) ) { Write-WhiskeyVerbose -Context $Context -Message ('ExceptOnPlatform ! {0} -eq {1}' -f $platform,$CurrentPlatform) $shouldSkip = $true break } else { Write-WhiskeyVerbose -Context $Context -Message ('ExceptOnPlatform {0} -ne {1}' -f $platform,$CurrentPlatform) } } return $shouldSkip } return $false } function Uninstall-WhiskeyNode { [CmdletBinding()] param( [Parameter(Mandatory)] # The directory where node is installed and from which it should be removed. [String]$InstallRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dirToRemove = Join-Path -Path $InstallRoot -ChildPath '.node' Remove-WhiskeyFileSystemItem -Path $dirToRemove } function Uninstall-WhiskeyNodeModule { <# .SYNOPSIS Uninstalls Node.js modules. .DESCRIPTION The `Uninstall-WhiskeyNodeModule` function will uninstall Node.js modules from the `node_modules` directory in the current working directory. It uses the `npm uninstall` command to remove the module. If the `npm uninstall` command fails to uninstall the module and the `Force` parameter was not used, then the function will write an error and return. If the `Force` parameter is used then the function will attempt to manually remove the module if `npm uninstall` fails. .EXAMPLE Uninstall-WhiskeyNodeModule -Name 'rimraf' -NodePath $TaskParameter['NodePath'] Removes the node module 'rimraf' from the `node_modules` directory in the current directory. .EXAMPLE Uninstall-WhiskeyNodeModule -Name 'rimraf' -NodePath $TaskParameter['NodePath'] -Force Removes the node module 'rimraf' from `node_modules` directory in the current directory. Because the `Force` switch is used, if `npm uninstall` fails, will attemp to use PowerShell to remove the module. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The name of the module to uninstall. [String]$Name, [Parameter(Mandatory)] # The path to the build root directory. [String]$BuildRootPath, # Node modules are being uninstalled on a developer computer. [switch]$ForDeveloper, # Remove the module manually if NPM fails to uninstall it [switch]$Force, # Uninstall the module from the global cache. [switch]$Global ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $argumentList = & { $Name if( $Global ) { '-g' } } Invoke-WhiskeyNpmCommand -Name 'uninstall' ` -BuildRootPath $BuildRootPath ` -ArgumentList $argumentList ` -ForDeveloper:$ForDeveloper $modulePath = Resolve-WhiskeyNodeModulePath -Name $Name -BuildRootPath $BuildRootPath -Global:$Global -ErrorAction Ignore if( $modulePath ) { if( $Force ) { Remove-WhiskeyFileSystemItem -Path $modulePath } else { Write-WhiskeyError -Message ('Failed to remove Node module "{0}" from "{1}". See previous errors for more details.' -f $Name,$modulePath) return } } if( $modulePath -and (Test-Path -Path $modulePath -PathType Container) ) { Write-WhiskeyError -Message ('Failed to remove Node module "{0}" from "{1}" using both "npm prune" and manual removal. See previous errors for more details.' -f $Name,$modulePath) return } } function Uninstall-WhiskeyPowerShellModule { <# .SYNOPSIS Removes downloaded PowerShell modules. .DESCRIPTION The `Uninstall-WhiskeyPowerShellModule` function deletes downloaded PowerShell modules from Whiskey's local "PSModules" directory. .EXAMPLE Uninstall-WhiskeyPowerShellModule -Name 'Pester' This example will uninstall the PowerShell module `Pester` from Whiskey's local `PSModules` directory. .EXAMPLE Uninstall-WhiskeyPowerShellModule -Name 'Pester' -ErrorAction Stop Demonstrates how to fail a build if uninstalling the module fails by setting the `ErrorAction` parameter to `Stop`. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The name of the module to uninstall. [String]$Name, [String]$Version = '*.*.*', [Parameter(Mandatory)] # Modules are saved into a PSModules directory. This is the path where the PSModules directory was created and should be the same path passed to `Install-WhiskeyPowerShellModule`. [String]$BuildRoot, # The directory where the module is installed. If this parameter is provided, the BuildRoot parameter is ignored. [String]$Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Get-Module -Name $Name | Remove-Module -Force $modulesRoot = Join-Path -Path $BuildRoot -ChildPath $script:psModulesDirectoryName if( $Path ) { $modulesRoot = $Path } # Remove modules saved by either PowerShell4 or PowerShell5 $moduleRoots = @( ('{0}\{1}' -f $Name, $Version) ) foreach ($item in $moduleRoots) { $removeModule = (Join-Path -Path $modulesRoot -ChildPath $item ) if( Test-Path -Path $removeModule -PathType Container ) { Remove-Item -Path $removeModule -Recurse -Force break } } if( (Test-Path -Path $modulesRoot -PathType Container) ) { $psmodulesDirEmpty = $null -eq (Get-ChildItem -Path $modulesRoot -File -Recurse) if( $psmodulesDirEmpty ) { Remove-Item -Path $modulesRoot -Recurse -Force } } } function Uninstall-WhiskeyTool { <# .SYNOPSIS Removes a tool installed with `Install-WhiskeyTool`. .DESCRIPTION The `Uninstall-WhiskeyTool` function removes tools that were installed with `Install-WhiskeyTool`. It removes PowerShell modules, NuGet packages, Node, Node modules, and .NET Core SDKs that Whiskey installs into your build root. PowerShell modules are removed from the `Modules` direcory. NuGet packages are removed from the `packages` directory. Node and node modules are removed from the `.node` directory. The .NET Core SDK is removed from the `.dotnet` directory. When uninstalling a Node module, its name should be prefixed with `NodeModule::`, e.g. `NodeModule::rimraf`. Users of the `Whiskey` API typcially won't need to use this function. It is called by other `Whiskey` function so they have the tools they need. .EXAMPLE Uninstall-WhiskeyTool -ModuleName 'Pester' Demonstrates how to remove the `Pester` module from the default location. .EXAMPLE Uninstall-WhiskeyTool -NugetPackageName 'NUnit.Runners' -Version '2.6.4' Demonstrates how to uninstall a specific NuGet Package. In this case, NUnit Runners version 2.6.4 would be removed from the default location. .EXAMPLE Uninstall-WhiskeyTool -ModuleName 'Pester' -Path $forPath Demonstrates how to remove a Pester module from a specified path location other than the default location. In this case, Pester would be removed from the directory pointed to by the $forPath variable. .EXAMPLE Uninstall-WhiskeyTool -ModuleName 'Pester' -DownloadRoot $Root Demonstrates how to remove a Pester module from a DownloadRoot. In this case, Pester would be removed from `$Root\Modules`. .EXAMPLE Uninstall-WhiskeyTool -Name 'Node' -BuildRoot $TaskContext.BuildRoot Demonstrates how to uninstall Node from the `.node` directory in your build root. .EXAMPLE Uninstall-WhiskeyTool -Name 'NodeModule::rimraf' -BuildRoot $TaskContext.BuildRoot Demonstrates how to uninstall the `rimraf` Node module from the `node_modules` directory in the Node directory in your build root. .EXAMPLE Uninstall-WhiskeyTool -Name 'DotNet' -BuildRoot $TaskContext.BuildRoot Demonstrates how to uninstall the .NET Core SDK from the `.dotnet` directory in your build root. #> [CmdletBinding()] param( # The tool attribute that defines what tool to uninstall. [Parameter(Mandatory, ParameterSetName='Tool')] [Whiskey.RequiresToolAttribute] $ToolInfo, # The name of the NuGet package to uninstall. [Parameter(Mandatory, ParameterSetName='NuGet')] [String] $NuGetPackageName, # The version of the package to uninstall. Must be a three part number, i.e. it must have a MAJOR, MINOR, and # BUILD number. [String] $Version, # The build root where the build is currently running. Tools are installed here. [String] $BuildRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState function Remove-NuGetPackage { $packagesRoot = Join-Path -Path $BuildRoot -ChildPath 'packages' $nuGetRootName = '{0}.{1}' -f $NuGetPackageName,$Version $nuGetRoot = Join-Path -Path $packagesRoot -ChildPath $nuGetRootName if( (Test-Path -Path $nuGetRoot -PathType Container) ) { Remove-Item -Path $nuGetRoot -Recurse -Force } } if( $PSCmdlet.ParameterSetName -eq 'NuGet' ) { Remove-NuGetPackage return } $provider = $ToolInfo.ProviderName $name = $ToolInfo.Name if( $ToolInfo -is [Whiskey.RequiresPowerShellModuleAttribute] ) { $provider = 'PowerShellModule' } switch( $provider ) { 'NodeModule' { # Don't do anything. All node modules require the Node tool to also be defined so they'll get deleted by # the Node deletion. } 'NuGet' { Remove-NuGetPackage } 'PowerShellModule' { Uninstall-WhiskeyPowerShellModule -Name $name -BuildRoot $BuildRoot } default { switch( $name ) { 'Node' { Uninstall-WhiskeyNode -InstallRoot $BuildRoot } 'DotNet' { $dotnetToolRoot = Join-Path -Path $BuildRoot -ChildPath '.dotnet' Remove-WhiskeyFileSystemItem -Path $dotnetToolRoot } default { throw ('Unknown tool "{0}". The only supported tools are "Node" and "DotNet".' -f $name) } } } } } function Unregister-WhiskeyEvent { <# .SYNOPSIS Unregisters a command to call when specific events happen during a build. .DESCRIPTION The `Unregister-WhiskeyEvent` function unregisters a command to run when a specific event happens during a build. This function is paired with `Register-WhiskeyEvent'. #> [CmdletBinding()] param( [Parameter(Mandatory)] # The context where the event should fire. [Whiskey.Context]$Context, [Parameter(Mandatory)] # The name of the command to run during the event. [String]$CommandName, [Parameter(Mandatory)] [ValidateSet('BeforeTask','AfterTask')] # When the command should be run; what events does it respond to? [String]$Event, # The specific task whose events to unregister. [String]$TaskName ) Set-StrictMode -Version 'Latest' $eventName = $Event if( $TaskName ) { $eventType = $Event -replace 'Task$','' $eventName = '{0}{1}Task' -f $eventType,$TaskName } $events = $Context.Events if( -not $events[$eventName] ) { return } if( -not $Events[$eventName].Contains( $CommandName ) ) { return } $events[$eventName].Remove( $CommandName ) } function Unregister-WhiskeyPSModulePath { [CmdletBinding()] param( [Parameter(Mandatory,ParameterSetName='FromUser')] [String]$Path, [Parameter(Mandatory,ParameterSetName='FromWhiskey')] [String]$PSModulesRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $PSCmdlet.ParameterSetName -eq 'FromWhiskey' ) { $Path = Get-WhiskeyPSModulePath -PSModulesRoot $PSModulesRoot } $pathBefore = $env:PSModulePath -split [IO.Path]::PathSeparator try { $modulePaths = $pathBefore | Where-Object { $_ -ne $Path } $env:PSModulePath = $modulePaths -join [IO.Path]::PathSeparator } finally { Write-WhiskeyDebug "[Unregister-WhiskeyPSModulePath] Changes to PSModulePath:" $pathNow = $env:PSModulePath -split [IO.Path]::PathSeparator $diff = Compare-Object -ReferenceObject $pathBefore -DifferenceObject $pathNow -IncludeEqual if( $diff ) { $diff | Format-Table -AutoSize | Out-String | Write-WhiskeyDebug } } } # Copyright 2012 Aaron Jensen # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. function Use-CallerPreference { <# .SYNOPSIS Sets the PowerShell preference variables in a module's function based on the callers preferences. .DESCRIPTION Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module. When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller: * ErrorAction * Debug * Confirm * InformationAction * Verbose * WarningAction * WhatIf This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function. This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d). There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every function/cmdlet call in your function. Please vote up this issue so it can get fixed. .LINK about_Preference_Variables .LINK about_CommonParameters .LINK https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d .LINK http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/ .EXAMPLE Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Demonstrates how to set the caller's common parameter preference variables in a module function. #> [CmdletBinding()] param ( [Parameter(Mandatory)] #[Management.Automation.PSScriptCmdlet] # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` attribute. $Cmdlet, [Parameter(Mandatory)] # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the `[CmdletBinding()]` attribute. # # Used to set variables in its callers' scope, even if that caller is in a different script module. [Management.Automation.SessionState]$SessionState ) Set-StrictMode -Version 'Latest' # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken from about_CommonParameters). $commonPreferences = @{ 'ErrorActionPreference' = 'ErrorAction'; 'DebugPreference' = 'Debug'; 'ConfirmPreference' = 'Confirm'; 'InformationPreference' = 'InformationAction'; 'VerbosePreference' = 'Verbose'; 'WarningPreference' = 'WarningAction'; 'WhatIfPreference' = 'WhatIf'; } foreach( $prefName in $commonPreferences.Keys ) { $parameterName = $commonPreferences[$prefName] # Don't do anything if the parameter was passed in. if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) ) { continue } $variable = $Cmdlet.SessionState.PSVariable.Get($prefName) # Don't do anything if caller didn't use a common parameter. if( -not $variable ) { continue } if( $SessionState -eq $ExecutionContext.SessionState ) { Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false } else { $SessionState.PSVariable.Set($variable.Name, $variable.Value) } } } function Write-CommandOutput { param( [Parameter(ValueFromPipeline)] [String]$InputObject, [Parameter(Mandatory)] [String]$Description ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $InputObject -match '^WARNING\b' ) { $InputObject | Write-WhiskeyWarning } elseif( $InputObject -match '^ERROR\b' ) { $InputObject | Write-WhiskeyError } else { $InputObject | ForEach-Object { '[{0}] {1}' -f $Description,$_ } | Write-WhiskeyVerbose } } } function Write-WhiskeyCommand { [CmdletBinding()] param( [Whiskey.Context] $Context, [String] $Path, [Object[]] $ArgumentList ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState # Might have passed array of arrays. $ArgumentList = & { $ArgumentList | ForEach-Object { $_ | Write-Output } } $logArgumentList = & { if( $Path -match '\ ' ) { '&' } $Path $ArgumentList } | Where-Object { $null -ne $_ } | ForEach-Object { if ($_ -match '\ |;' -or $_ -eq '') { '"{0}"' -f $_.Trim('"',"'") } else { $_ } } Write-WhiskeyInfo -Context $Context -Message ($logArgumentList -join ' ') Write-WhiskeyVerbose -Context $Context -Message $Path $argumentPrefix = ' ' foreach( $argument in $ArgumentList ) { Write-WhiskeyVerbose -Context $Context -Message ('{0}{1}' -f $argumentPrefix,$argument) } } function Write-WhiskeyDebug { <# .SYNOPSIS Logs debug messages. .DESCRIPTION The `Write-WhiskeyDebug` function writes debug messages using PowerShell's `Write-Debug` cmdlet. Pass the context of the current build to the `Context` parameter and the message to write to the `Message` parameter. Messages are prefixed with the duration of the current build and curren task. If the duration can't be determined, the current time is used. If `$DebugPreference` is set to `SilentlyContinue` or `Ignore`, `Write-WhiskeyDebug` immediately returns. You can pass messages to the `Message` parameter, or pipe messages to `Write-WhiskeyDebug`. To view debug messages in your build output, you'll need to set the global `DebugPreference` variable to `Continue`. You can also log error, warning, info, and verbose messages with Whiskey's `Write-WhiskeyError`, `Write-WhiskeyWarning`, `Write-WhiskeyInfo`, and `Write-WhiskeyVerbose` functions. .EXAMPLE Write-WhiskeyDebug -Context $context -Message 'My debug message' Demonstrates how to write a debug message. .EXAMPLE $messages | Write-WhiskeyDebug -Context $context Demonstrates how to pipe messages to `Write-WhiskeyDebug`. #> [CmdletBinding(DefaultParameterSetName='NoIndent')] param( # The context of the current build. [Whiskey.Context]$Context, [Parameter(Mandatory,ValueFromPipeline,Position=0)] [AllowEmptyString()] [AllowNull()] # The message to write. Before being written, the message will be prefixed with the duration of the current build and the current task name (if any). If the current duration can't be determined, then the current time is used. # # If you pipe multiple messages, they are grouped together. [String]$Message, [Parameter(Mandatory, ParameterSetName='Indent')] [switch] $Indent, [Parameter(Mandatory, ParameterSetName='Outdent')] [switch] $Outdent ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $Outdent ) { $script:indentLevel -= 1 } $write = $DebugPreference -notin @( [Management.Automation.ActionPreference]::Ignore, [Management.Automation.ActionPreference]::SilentlyContinue ) if( -not $write ) { return } $messages = $null if( $PSCmdlet.MyInvocation.ExpectingInput ) { $messages = [Collections.ArrayList]::new() } } process { if( -not $write ) { return } if( $indentLevel -gt 0 ) { $prefix = ' ' * ($indentLevel * 4) foreach( $line in ($Message -split '\n\r?') ) { $newMsg = "$($prefix)$($line)" if( $PSCmdlet.MyInvocation.ExpectingInput ) { [void]$messages.Add($newMsg) return } Write-WhiskeyInfo -Context $Context -Message $newMsg -Level 'Debug' } return } if( $PSCmdlet.MyInvocation.ExpectingInput ) { [void]$messages.Add($Message) return } Write-WhiskeyInfo -Context $Context -Message $Message -Level 'Debug' } end { try { if( -not $write ) { return } if( $messages ) { Write-WhiskeyInfo -Context $Context -Level 'Debug' -Message $messages } } finally { if( $Indent ) { $script:indentLevel += 1 } } } } function Write-WhiskeyError { <# .SYNOPSIS Logs error messages. .DESCRIPTION The `Write-WhiskeyError` function writes error messages using PowerShell's `Write-Error` cmdlet. Pass the context of the current build to the `Context` parameter and the message you want to write to the `Message` parameter. Error messages are prefixed with the duration of the current build and current task. You may pass multiple message to the `Message` parameter or pipe messages to `Write-WhiskeyError`. All messages are joined together with newlines before `Write-Error` is called. By default, error messages do *not* stop a build. If you want to log an error *and* fail/stop a build, use the `Stop-WhiskeyTask` function. If `$ErrorActionPreference` is `Ignore`, `Write-WhiskeyError` does no work and immediately returns Whiskey ships with its own error output formatter that will show the entire script stack trace of an error. You'll get this view even if you don't use `Write-WhiskeyError`. You can also log warning, info, verbose, and debug messages with Whiskey's `Write-WhiskeyWarning`, `Write-WhiskeyInfo`, `Write-WhiskeyVerbose`, and `Write-WhiskeyDebug` functions. .EXAMPLE Write-WhiskeyError -Context $context -Message 'Something bad happened!' Demonstrates how to write an error. .EXAMPLE $errors | Write-WhiskeyError -Context $context Demonstrates that you can pipe messages to `Write-WhiskeyError`. If you do, all the messages will be combined with a newline before calling `Write-Error`. #> [CmdletBinding()] param( # The context for the current build. If not provided, Whiskey will search up the call stack looking for it. [Whiskey.Context]$Context, [Parameter(Mandatory,ValueFromPipeline,Position=0)] [AllowNull()] [AllowEmptyString()] # The message to write. Each message is written to the user with `Write-Error`. [String]$Message ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $write = $ErrorActionPreference -ne [Management.Automation.ActionPreference]::Ignore if( -not $write ) { return } [Collections.ArrayList]$messages = $null if( $PSCmdlet.MyInvocation.ExpectingInput ) { $messages = [Collections.ArrayList]::new() } } process { if( -not $write ) { return } if( $PSCmdlet.MyInvocation.ExpectingInput ) { [Void]$messages.Add($Message) return } Write-WhiskeyInfo -Context $Context -Level Error -Message $Message } end { if( -not $write ) { return } if( $messages ) { Write-WhiskeyInfo -Context $Context -Level Error -Message $messages } } } function Write-WhiskeyInfo { <# .SYNOPSIS Logs informational messages. .DESCRIPTION The `Write-WhiskeyInfo` function writes informational messages during a build using PowerShell's `Write-Information` cmdlet. Pass the current build's context object to the `Context` parameter and the message to write to the `Message` parameter. Messages are prefixed with the duration of the current build and current task. By default, Whiskey sets the `InformationPreference` to `Continue` for all builds so all information messages will be visible. To hide information messages, you must call `Invoke-WhiskeyBuild` with `-InformationAction` set to `Ignore`. You may pass multiple messages to the `Message` property, or pipe messages to `Write-WhiskeyInfo`. If `$InformationPreference` is `Ignore`, Whiskey does no work and immediately returns. You can also log error, warning, verbose, and debug messages with Whiskey's `Write-WhiskeyError`, `Write-WhiskeyWarning`, `Write-WhiskeyVerbose`, and `Write-WhiskeyDebug` functions. You can use Whiskey's `Log` task to log messages at different levels. .EXAMPLE Write-WhiskeyInfo -Context $context -Message 'An info message' Demonstrates how write an `Info` message. .EXAMPLE $output | Write-WhiskeyInfo -Context $context Demonstrates that you can pipe messages to `Write-WhiskeyInfo`. #> [CmdletBinding()] param( # The context for the current build. If not provided, Whiskey will search up the call stack looking for it. [Whiskey.Context] $Context, [ValidateSet('Error', 'Warning', 'Info', 'Verbose', 'Debug')] # INTERNAL. DO NOT USE. To log at different levels, use `Write-WhiskeyError`, `Write-WhiskeyWarning`, # `Write-WhiskeyVerbose`, or `Write-WhiskeyDebug` [String] $Level = 'Info', [Parameter(Mandatory,ValueFromPipeline,Position=0)] [AllowNull()] [AllowEmptyString()] # The message to write. Before being written, the message will be prefixed with the duration of the current # build and the current task name (if any). If the current duration can't be determined, then the current time # is used. # # If you pipe multiple messages, they are grouped together. [String[]] $Message, [switch] $NoIndent, [switch] $NoTiming ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $isError = $Level -eq 'Error' $isInfo = $Level -eq 'Info' $isWarn = $Level -eq 'Warning' $isVerbose = $Level -eq 'Verbose' $isDebug = $Level -eq 'Debug' # Only write if absolutely necessary as it can be expensive. # Since silent errors, warnings, and info can be captured, we still have to output when their prefs are silent. # Silent verbose and debug messages are currently capturable, so don't even bother to write them. $write = ($isError -and $ErrorActionPreference -ne [Management.Automation.ActionPreference]::Ignore) -or ($IsWarn -and $WarningPreference -ne [Management.Automation.ActionPreference]::Ignore) -or ($isInfo -and $InformationPreference -ne [Management.Automation.ActionPreference]::Ignore) -or ($isVerbose -and $VerbosePreference -notin @([Management.Automation.ActionPreference]::Ignore,[Management.Automation.ActionPreference]::SilentlyContinue)) -or ($isDebug -and $DebugPreference -notin @([Management.Automation.ActionPreference]::Ignore,[Management.Automation.ActionPreference]::SilentlyContinue)) if( -not $write ) { return } $errorMsgs = [Collections.ArrayList]::New() if( $isError ) { return } # Don't put timings in error or warning messages if ($IsError -or $isWarn) { $NoTiming = $true } $writeCmd = 'Write-{0}' -f $Level if( $isInfo ) { $writeCmd = 'Write-Information' } if( -not $Context ) { $Context = Get-WhiskeyContext } $prefix = '' $indent = $taskWriteIndent if( $NoIndent -or -not $isInfo ) { $indent = '' } } process { if( -not $write ) { return } foreach( $msg in $Message ) { if( $isError ) { [void]$errorMsgs.Add($msg) continue } $prefix = '' if (-not $NoTiming) { if( $Context ) { $prefix = "[$($Context.BuildStopwatch | Format-Stopwatch)] [$($Context.TaskStopwatch | Format-Stopwatch)]" } else { $prefix = "[$((Get-Date).ToString('HH:mm:ss'))]" } } $separator = ' ' $thisMsgIndent = $indent if (-not $msg -or $isWarn) { $separator = '' $thisMsgIndent = '' } if (-not $isInfo) { $thisMsgIndent = '' } & $writeCmd "$($prefix)$($separator)$($thisMsgIndent)$($msg)" } } end { if( $write -and $isError -and $errorMsgs.Count ) { $errorMsg = $errorMsgs -join [Environment]::NewLine Write-Error -Message $errorMsg } } } function Write-WhiskeyObject { <# .SYNOPSIS Writes objects as recognizable strings. .DESCRIPTION The `Write-WhiskeyObject` function writes objects as recognizable strings. Use the `Level` parameter to control what write function to use (see the help for `Write-WhiskeyInfo` for more information). It supports hashtables and dictionaries. It writes the keys and values in separate columns. If a value contains multiple values, each value is aligned with previous values. For example: OutputFile .output\pester.xml OutputFormat JUnitXml PassThru True Script .\PassingTests.ps1 .\OtherPassingTests.ps1 Show None TestName PassingTests .EXAMPLE $hashtable | Write-WhiskeyObject -Context $Context -Level Verbose Demonstrates how to print the value of a hashtable in a recognizable format. .EXAMPLE $hashtable | Write-WhiskeyObject -Level Verbose Demonstrates that the `Context` parameter is optional. Whiskey searches up the call stack to find one if you don't pass it. #> [CmdletBinding()] param( # The context for the current build. If not provided, Whiskey will search up the call stack looking for it. [Whiskey.Context]$Context, [ValidateSet('Error','Warning','Info','Verbose','Debug')] # The level at which to write the object. The default is `Info`. [String]$Level = 'Info', [Parameter(Mandatory,ValueFromPipeline,Position=0)] [AllowNull()] [AllowEmptyString()] # The message/object to write. Before being written, the message will be prefixed with the duration of the current build and the current task name (if any). If the current duration can't be determined, then the current time is used. # # If you pipe multiple messages, they are grouped together. [Object]$InputObject ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $objects = [Collections.ArrayList]::new() function ConvertTo-String { param( [Parameter(Mandatory,ValueFromPipeline)] [AllowNull()] [AllowEmptyString()] [AllowEmptyCollection()] [Object]$InputObject ) process { if( $null -eq $InputObject ) { return '$null' } if( ($InputObject | Measure-Object).Count -gt 1 ) { $output = New-Object 'Text.StringBuilder' '@( ' $values = $InputObject | ConvertTo-String $values = $values -join ', ' [void]$output.Append($values) [void]$output.Append(' )') return $output.ToString() } if( $InputObject | Get-Member 'Keys' ) { $output = New-Object 'Text.StringBuilder' '@{ ' $values = & { foreach( $key in $InputObject.Keys ) { $value = ConvertTo-String -InputObject $InputObject[$key] $key = $key | ConvertTo-String Write-Output ('{0} = {1}' -f $key,$value) } } [void]$output.Append(($values -join '; ')) [void]$output.Append(' }') return $output.ToString() } if( $InputObject -is [String] -and $InputObject -ne '$null' ) { return ("'{0}'" -f ($InputObject -replace "'","''")) } return $InputObject.ToString() } } } process { [void]$objects.Add($InputObject) } end { & { foreach( $object in $objects ) { if( $object | Get-Member 'Keys' ) { $maxKeyLength = $object.Keys | ForEach-Object { $_.ToString().Length } | Sort-Object -Descending | Select-Object -First 1 $formatString = '{{0,-{0}}} {{1}}' -f $maxKeyLength foreach( $key in ($object.Keys | Sort-Object) ) { $value = $object[$key] $firstValue = ConvertTo-String ($value | Select-Object -First 1) Write-Output ($formatString -f $key,$firstValue) $value | Select-Object -Skip 1 | ForEach-Object { $formatString -f ' ',(ConvertTo-String $_) } } } else { $object | Out-String } } } | Write-WhiskeyInfo -Context $Context -Level $Level } } function Write-WhiskeyVerbose { <# .SYNOPSIS Logs verbose messages. .DESCRIPTION The `Write-WhiskeyVerbose` function writes verbose messages with PowerShell's `Write-Verbose` cmdlet. Pass the context of the current build to the `Context` parameter and the message to log to the `Message` parameter. Each message is prefixed with the duration of the current build and current task. Multiple messages may be passed to the `Message` parameter or piped to `Write-WhiskeyVerbose`. If `$VerbosePreference` is set to `SilentlyContinue` or `Ignore`, `Write-WhiskeyVerbose` does no work and immediately returns To see verbose messages in your build output, use the `-Verbose` switch when running your build. You can also log error, warning, info, and debug messages with Whiskey's `Write-WhiskeyError`, `Write-WhiskeyWarning`, `Write-WhiskeyInfo`, and `Write-WhiskeyDebug` functions. .EXAMPLE Write-WhiskeyVerbose -Context $context -Message 'My verbose message' Demonstrates how to write a verbose message. .EXAMPLE $messages | Write-WhiskeyVerbose -Context $context Demonstrates that you can pipe messages to `Write-WhiskeyVerbose`. #> [CmdletBinding()] param( # The current context. [Whiskey.Context]$Context, [Parameter(Mandatory,ValueFromPipeline,Position=0)] [AllowEmptyString()] [AllowNull()] # The message to write. Before being written, the message will be prefixed with the duration of the current build and the current task name (if any). If the current duration can't be determined, then the current time is used. # # If you pipe multiple messages, they are grouped together. [String]$Message ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $write = $VerbosePreference -notin @( [Management.Automation.ActionPreference]::Ignore, [Management.Automation.ActionPreference]::SilentlyContinue ) if( -not $write ) { return } $messages = $null if( $PSCmdlet.MyInvocation.ExpectingInput ) { $messages = [Collections.ArrayList]::new() } } process { if( -not $write ) { return } if( $PSCmdlet.MyInvocation.ExpectingInput ) { [Void]$messages.Add($Message) return } Write-WhiskeyInfo -Context $Context -Message $Message -Level 'Verbose' } end { if( -not $write ) { return } if( $messages ) { Write-WhiskeyInfo -Context $Context -Level 'Verbose' -Message $messages } } } function Write-WhiskeyWarning { <# .SYNOPSIS Logs warning messages. .DESCRIPTION The `Write-WhiskeyWarning` function writes warning messages using PowerShell's `Write-Warning` cmdlet. Pass the context of the current build to the `Context` parameter and the message to write to the `Message` parameter. Messages are prefixed with the duration of the current build and task. Multiple messages may be passed to the `Message` parameter or piped to `Write-WhiskeyWarning`. If the `$WarningPreference` is `Ignore`, `Write-WhiskeyWarning` does no work and immediately returns. You can also log error, info, verbose, and debug messages with Whiskey's `Write-WhiskeyError`, `Write-WhiskeyInfo`, `Write-WhiskeyVerbose`, and `Write-WhiskeyDebug` functions. .EXAMPLE Write-WhiskeyWarning -Context $context -Message 'My warning!' Demonstrates how write a `Warning` message. .EXAMPLE $messages | Write-WhiskeyWarning -Context $context Demonstrates that you can pipe messages to `Write-WhiskeyWarning`. #> [CmdletBinding()] param( # The current context. [Whiskey.Context]$Context, [Parameter(Mandatory,ValueFromPipeline,Position=0)] # The message to write. Before being written, the message will be prefixed with the duration of the current build and the current task name (if any). If the current duration can't be determined, then the current time is used. # # If you pipe multiple messages, they are grouped together. [String]$Message ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $write = $WarningPreference -ne [Management.Automation.ActionPreference]::Ignore if( -not $write ) { return } $messages = $null if( $PSCmdlet.MyInvocation.ExpectingInput ) { $messages = [Collections.ArrayList]::new() } } process { if( -not $write ) { return } if( $PSCmdlet.MyInvocation.ExpectingInput ) { [Void]$messages.Add($Message) return } Write-WhiskeyInfo -Context $Context -Level 'Warning' -Message $Message } end { if( -not $write ) { return } if( $messages ) { Write-WhiskeyInfo -Context $Context -Level 'Warning' -Message $messages } } } function Wait-WhiskeyAppVeyorBuildJob { [CmdletBinding()] [Whiskey.Task('AppVeyorWaitForBuildJobs')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [String]$ApiKeyID, [TimeSpan]$CheckInterval = '00:00:10', [TimeSpan]$ReportInterval = '00:01:00', [String[]]$InProgressStatus = @('running','queued'), [String[]]$SuccessStatus = @('success') ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not (Test-Path -Path 'env:APPVEYOR') ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Not running under AppVeyor.') return } $bearerToken = Get-WhiskeyApiKey -Context $TaskContext -ID $ApiKeyID -PropertyName 'ApiKeyID' $headers = @{ 'Authorization' = ('Bearer {0}' -f $bearerToken); 'Content-Type' = 'application/json'; } $accountName = (Get-Item -Path 'env:APPVEYOR_ACCOUNT_NAME').Value $slug = (Get-Item -Path 'env:APPVEYOR_PROJECT_SLUG').Value $myBuildId = (Get-Item -Path 'env:APPVEYOR_BUILD_ID').Value $buildUri = 'https://ci.appveyor.com/api/projects/{0}/{1}/builds/{2}' -f $accountName,$slug,$myBuildId $myJobId = (Get-Item -Path 'env:APPVEYOR_JOB_ID').Value $nextOutput = [Diagnostics.StopWatch]::new() # Eventually AppVeyor will time us out. while( $true ) { $ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue $result = Invoke-RestMethod -Uri $buildUri -Method Get -Headers $headers -Verbose:$false $result | ConvertTo-Json -Depth 100 | Write-Debug if( -not $result -or -not $result.build ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failed to retrieve current build status from {0}: the request returned no build information:{1} {1}{2}.' -f $buildUri,[Environment]::NewLine,($result | ConvertTo-Json -Depth 100)) return } $build = $result.build # Skip this job. $jobsToCheck = $build.jobs | Where-Object { $_ } | Where-Object { $_.jobId -ne $myJobId } $unfinishedJobs = $jobsToCheck | # Jobs currently running don't have a 'finished' member. Just in case that changes in the future, also check status. Where-Object { -not ($_ | Get-Member 'finished') -or $_.status -in $InProgressStatus } | ForEach-Object { if( -not $nextOutput.IsRunning -or $ReportInterval -lt $nextOutput.Elapsed ) { Write-WhiskeyInfo -Context $TaskContext -Message ('"{0}" job is {1}.' -f $_.name,$_.status) $nextOutput.Restart() } $_ } if( $unfinishedJobs ) { Start-Sleep -Milliseconds $CheckInterval.TotalMilliseconds continue } $failedJobs = $jobsToCheck | Where-Object { $_.status -notin $SuccessStatus } if( $failedJobs ) { $suffix = '' if( ($failedJobs | Measure-Object).Count -gt 1 ) { $suffix = 's' } $jobDescriptions = $failedJobs | ForEach-Object { '{0} (status: {1})' -f $_.name,$_.status} $jobDescriptions = $jobDescriptions -join ('{0} * ' -f [Environment]::NewLine) $errorMsg = 'This build''s other job{0} did not succeed.{1} {1} * {2} {1} ' -f $suffix, [Environment]::NewLine, $jobDescriptions Stop-WhiskeyTask -TaskContext $TaskContext -Message $errorMsg return } break } } function Copy-WhiskeyFile { [Whiskey.Task('CopyFile')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')] [String[]]$Path, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='Directory',AllowNonexistent,Create)] [String[]]$DestinationDirectory ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $DestinationDirectory ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property "DestinationDirectory" didn''t resolve to any existing paths.' return } foreach( $destDir in $DestinationDirectory ) { foreach($sourceFile in $Path) { Write-WhiskeyInfo -Context $TaskContext -Message ('{0} -> {1}' -f $sourceFile,$destDir) Copy-Item -Path $sourceFile -Destination $destDir } } } function Remove-WhiskeyItem { [Whiskey.TaskAttribute('Delete',SupportsClean)] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Whiskey.Tasks.ValidatePath(Mandatory,AllowNonexistent)] [String[]]$Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState foreach( $pathItem in $Path ) { if( -not (Test-Path -Path $pathItem) ) { continue } Remove-WhiskeyFileSystemItem -Path $pathitem -ErrorAction Stop } } function Invoke-WhiskeyDotNet { [CmdletBinding()] [Whiskey.Task('DotNet')] [Whiskey.RequiresTool('DotNet', PathParameterName='DotNetPath', VersionParameterName='SdkVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [switch] $NoLog ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $command = $TaskParameter['Command'] if( -not $command ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Command" is required. It should be the name of the dotnet.exe command to run, e.g. "build", "test", etc.') return } $dotnetExe = $TaskParameter['DotNetPath'] $invokeParameters = @{ TaskContext = $TaskContext; Name = $command; ArgumentList = $TaskParameter['Argument']; NoLog = $NoLog; } if ( $TaskParameter.ContainsKey('DotNetPath') ) { $invokeParameters['DotNetPath'] = $TaskParameter['DotNetPath'] } Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version)) if( $TaskParameter.ContainsKey('Path') ) { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' -PathType 'File' -AllowNonexistent if( -not $projectPaths -and (Get-Location).Path -ne $TaskContext.BuildRoot ) { Push-Location $TaskContext.BuildRoot try { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' -PathType 'File' | Resolve-Path | Select-Object -ExpandProperty 'ProviderPath' } finally { Pop-Location } if( $projectPaths ) { Write-WhiskeyWarning -Context $TaskContext -Message ('Property Path: Paths are now resolved relative to a task''s working directory, not the build root. Please update the paths in your whiskey.yml file so they are relative to the DotNet task''s working directory.') $projectPaths = $projectPaths | Resolve-Path -Relative } } foreach( $projectPath in $projectPaths ) { Invoke-WhiskeyDotNetCommand @invokeParameters -ProjectPath $projectPath } } else { Invoke-WhiskeyDotNetCommand @invokeParameters } } function Invoke-WhiskeyDotNetBuild { [CmdletBinding()] [Whiskey.Task('DotNetBuild',Obsolete,ObsoleteMessage='The "DotNetTest" task is obsolete and will be removed in a future version of Whiskey. Please use the "DotNet" task instead.')] [Whiskey.RequiresTool('DotNet',PathParameterName='DotNetPath',VersionParameterName='SdkVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotnetExe = $TaskParameter['DotNetPath'] $projectPaths = '' if ($TaskParameter['Path']) { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPathInternal -TaskContext $TaskContext -PropertyName 'Path' } $verbosity = $TaskParameter['Verbosity'] if( -not $verbosity ) { $verbosity = 'minimal' } $dotnetArgs = & { '--configuration={0}' -f (Get-WhiskeyMSBuildConfiguration -Context $TaskContext) '-p:Version={0}' -f $TaskContext.Version.SemVer1.ToString() if ($verbosity) { '--verbosity={0}' -f $verbosity } if ($TaskParameter['OutputDirectory']) { '--output={0}' -f $TaskParameter['OutputDirectory'] } if ($TaskParameter['Argument']) { $TaskParameter['Argument'] } } Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version)) foreach($project in $projectPaths) { Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath $dotnetExe -Name 'build' -ArgumentList $dotnetArgs -ProjectPath $project } } function Invoke-WhiskeyDotNetPack { [CmdletBinding()] [Whiskey.Task('DotNetPack',Obsolete,ObsoleteMessage='The "DotNetTest" task is obsolete and will be removed in a future version of Whiskey. Please use the "DotNet" task instead.')] [Whiskey.RequiresTool('DotNet',PathParameterName='DotNetPath',VersionParameterName='SdkVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotnetExe = $TaskParameter['DotNetPath'] $projectPaths = '' if ($TaskParameter['Path']) { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPathInternal -TaskContext $TaskContext -PropertyName 'Path' } $symbols = $TaskParameter['Symbols'] | ConvertFrom-WhiskeyYamlScalar $verbosity = $TaskParameter['Verbosity'] if (-not $verbosity) { $verbosity = 'minimal' } $dotnetArgs = & { '-p:PackageVersion={0}' -f $TaskContext.Version.SemVer1.ToString() '--configuration={0}' -f (Get-WhiskeyMSBuildConfiguration -Context $TaskContext) '--output={0}' -f $TaskContext.OutputDirectory '--no-build' '--no-dependencies' '--no-restore' if ($symbols) { '--include-symbols' } if ($verbosity) { '--verbosity={0}' -f $verbosity } if ($TaskParameter['Argument']) { $TaskParameter['Argument'] } } Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version)) foreach($project in $projectPaths) { Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath $dotnetExe -Name 'pack' -ArgumentList $dotnetArgs -ProjectPath $project } } function Invoke-WhiskeyDotNetPublish { [CmdletBinding()] [Whiskey.Task('DotNetPublish',Obsolete,ObsoleteMessage='The "DotNetPublish" task is obsolete and will be removed in a future version of Whiskey. Please use the "DotNet" task instead.')] [Whiskey.RequiresTool('DotNet',PathParameterName='DotNetPath',VersionParameterName='SdkVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotnetExe = $TaskParameter['DotNetPath'] $projectPaths = '' if ($TaskParameter['Path']) { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPathInternalInternal -TaskContext $TaskContext -PropertyName 'Path' } $verbosity = $TaskParameter['Verbosity'] if (-not $verbosity) { $verbosity = 'minimal' } $dotnetArgs = & { '--configuration={0}' -f (Get-WhiskeyMSBuildConfiguration -Context $TaskContext) '-p:Version={0}' -f $TaskContext.Version.SemVer1.ToString() if ($verbosity) { '--verbosity={0}' -f $verbosity } if ($TaskParameter['OutputDirectory']) { '--output={0}' -f $TaskParameter['OutputDirectory'] } if ($TaskParameter['Argument']) { $TaskParameter['Argument'] } } Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version)) foreach($project in $projectPaths) { Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath $dotnetExe -Name 'publish' -ArgumentList $dotnetArgs -ProjectPath $project } } function Invoke-WhiskeyDotNetTest { [CmdletBinding()] [Whiskey.Task('DotNetTest',Obsolete,ObsoleteMessage='The "DotNetTest" task is obsolete and will be removed in a future version of Whiskey. Please use the "DotNet" task instead.')] [Whiskey.RequiresTool('DotNet',PathParameterName='DotNetPath',VersionParameterName='SdkVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotnetExe = $TaskParameter['DotNetPath'] $projectPaths = '' if ($TaskParameter['Path']) { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPathInternal -TaskContext $TaskContext -PropertyName 'Path' } $verbosity = $TaskParameter['Verbosity'] if (-not $verbosity) { $verbosity = 'minimal' } $dotnetArgs = & { '--configuration={0}' -f (Get-WhiskeyMSBuildConfiguration -Context $TaskContext) '--no-build' '--results-directory={0}' -f ($TaskContext.OutputDirectory.FullName) if ($Taskparameter['Filter']) { '--filter={0}' -f $TaskParameter['Filter'] } if ($TaskParameter['Logger']) { '--logger={0}' -f $TaskParameter['Logger'] } if ($verbosity) { '--verbosity={0}' -f $verbosity } if ($TaskParameter['Argument']) { $TaskParameter['Argument'] } } Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version)) foreach($project in $projectPaths) { Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath $dotnetExe -Name 'test' -ArgumentList $dotnetArgs -ProjectPath $project } } function Invoke-WhiskeyExec { [CmdletBinding()] [Whiskey.Task('Exec',SupportsClean,SupportsInitialize)] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $TaskParameter.ContainsKey('') ) { $regExMatches = Select-String -InputObject $TaskParameter[''] -Pattern '([^\s"'']+)|("[^"]*")|(''[^'']*'')' -AllMatches $defaultProperty = @($regExMatches.Matches.Groups | Where-Object { $_.Name -ne '0' -and $_.Success -eq $true } | Select-Object -ExpandProperty 'Value') $TaskParameter['Path'] = $defaultProperty[0] if( $defaultProperty.Count -gt 1 ) { $TaskParameter['Argument'] = $defaultProperty[1..($defaultProperty.Count - 1)] | ForEach-Object { $_.Trim("'",'"') } } } $path = $TaskParameter['Path'] if ( -not $path ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Path" is mandatory. It should be the Path to the executable you want the Exec task to run, e.g. Build: - Exec: Path: cmd.exe ') return } $path = & { if( [IO.Path]::IsPathRooted($path) ) { $path } else { Join-Path -Path (Get-Location).Path -ChildPath $path Join-Path -Path $TaskContext.BuildRoot -ChildPath $path } } | Where-Object { Test-Path -path $_ -PathType Leaf } | Select-Object -First 1 | Resolve-Path | Select-Object -ExpandProperty 'ProviderPath' if( -not $path ) { $path = $TaskParameter['Path'] if( -not (Get-Command -Name $path -CommandType Application -ErrorAction Ignore) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Executable "{0}" does not exist. We checked if the executable is at that path on the file system and if it is in your PATH environment variable.' -f $path) return } } if( ($path | Measure-Object).Count -gt 1 ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to run executable "{0}": it contains wildcards and resolves to the following files: "{1}".' -f $TaskParameter['Path'],($path -join '","')) return } Write-WhiskeyCommand -Context $TaskContext -Path $path -ArgumentList $TaskParameter['Argument'] # Don't use Start-Process. If/when a build runs in a background job, when Start-Process finishes, it immediately terminates the build. Full stop. & $path $TaskParameter['Argument'] $exitCode = $LASTEXITCODE $successExitCodes = $TaskParameter['SuccessExitCode'] if( -not $successExitCodes ) { $successExitCodes = '0' } foreach( $successExitCode in $successExitCodes ) { if( $successExitCode -match '^(\d+)$' ) { if( $exitCode -eq [int]$Matches[0] ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} = {1}' -f $exitCode,$Matches[0]) return } } if( $successExitCode -match '^(<|<=|>=|>)\s*(\d+)$' ) { $operator = $Matches[1] $successExitCode = [int]$Matches[2] switch( $operator ) { '<' { if( $exitCode -lt $successExitCode ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} < {1}' -f $exitCode,$successExitCode) return } } '<=' { if( $exitCode -le $successExitCode ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} <= {1}' -f $exitCode,$successExitCode) return } } '>' { if( $exitCode -gt $successExitCode ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} > {1}' -f $exitCode,$successExitCode) return } } '>=' { if( $exitCode -ge $successExitCode ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} >= {1}' -f $exitCode,$successExitCode) return } } } } if( $successExitCode -match '^(\d+)\.\.(\d+)$' ) { if( $exitCode -ge [int]$Matches[1] -and $exitCode -le [int]$Matches[2] ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} <= {1} <= {2}' -f $Matches[1],$exitCode,$Matches[2]) return } } } Stop-WhiskeyTask -TaskContext $TaskContext -Message ('"{0}" returned with an exit code of "{1}". View the build output to see why the executable''s process failed.' -F $TaskParameter['Path'],$exitCode) } function New-WhiskeyFile { [Whiskey.Task('File')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File',AllowNonexistent)] [String[]]$Path, [String]$Content, [switch]$Touch ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState foreach( $item in $Path ) { if( Test-Path -Path $item -PathType Container ) { Stop-WhiskeyTask -TaskContext $TaskContext ` -Message ('Path "{0}" is a directory but must be a file.' -f $item) return } if( -not (Test-Path -Path $item) ) { New-Item -Path $item -Force -ErrorAction Stop } if( $Touch ) { (Get-Item $item).LastWriteTime = Get-Date } if( $Content ) { Set-Content -Path $item -Value $Content } } } function Get-WhiskeyPowerShellModule { [CmdletBinding()] [Whiskey.Task('GetPowerShellModule', SupportsClean, SupportsInitialize)] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [String]$Name, [String]$Version, [switch]$AllowPrerelease, [Whiskey.Tasks.ValidatePath(AllowNonexistent,Create,PathType='Directory')] [String]$Path, [switch]$Import ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $Name ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property "Name" is mandatory. It should be set to the name of the PowerShell module you want installed.' return } if( $TaskContext.ShouldClean ) { Uninstall-WhiskeyPowerShellModule -Name $Name -BuildRoot $TaskContext.BuildRoot -Path $Path return } if( -not $Path ) { $Path = Get-WhiskeyPSModulePath -PSModulesRoot $TaskContext.BuildRoot -Create $Path = $Path | Resolve-Path -Relative } $module = Find-WhiskeyPowerShellModule -Name $Name ` -Version $Version ` -BuildRoot $TaskContext.BuildRoot ` -AllowPrerelease:$AllowPrerelease ` -ErrorAction Stop if( -not $module ) { return } # PackageManagement/PowerShellGet functions don't like relative paths. $fullPath = $Path | Resolve-Path | Select-Object -ExpandProperty 'ProviderPath' Write-WhiskeyInfo -Context $TaskContext -Message ('Installing PowerShell module {0} {1} to {2}.' -f $Name,$module.Version,$Path) $moduleRoot = Install-WhiskeyPowerShellModule -Name $Name ` -Version $module.Version ` -BuildRoot $TaskContext.BuildRoot ` -SkipImport:(-not $Import) ` -AllowPrerelease:$AllowPrerelease ` -Path $fullPath ` -ErrorAction Stop Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $moduleRoot) } function New-WhiskeyGitHubRelease { [CmdletBinding()] [Whiskey.Task('GitHubRelease')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $apiKeyID = $TaskParameter['ApiKeyID'] if( -not $apiKeyID ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "ApiKeyID" is mandatory. It should be set to the ID of the API key to use when talking to the GitHub API. API keys are added to your build with the "Add-WhiskeyApiKey" function.') return } $apiKey = Get-WhiskeyApiKey -Context $TaskContext -ID $apiKeyID -PropertyName 'ApiKeyID' $headers = @{ Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($apiKey + ":x-oauth-basic")) } $repositoryName = $TaskParameter['RepositoryName'] if( -not $repositoryName ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "RepositoryName" is mandatory. It should be the owner and repository name of the repository you want to access as a URI path, e.g. OWNER/REPO.') return } if( $repositoryName -notmatch '^[^/]+/[^/]+$' ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "RepositoryName" is invalid. It should be the owner and repository name of the repository you want to access as a URI path, e.g. OWNER/REPO.') return } $baseUri = [Uri]'https://api.github.com/repos/{0}' -f $repositoryName [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 function Invoke-GitHubApi { [CmdletBinding(DefaultParameterSetName='NoBody')] param( [Parameter(Mandatory)] [Uri]$Uri, [Parameter(Mandatory,ParameterSetName='FileUpload')] [String]$ContentType, [Parameter(Mandatory,ParameterSetName='FileUpload')] [String]$InFile, [Parameter(Mandatory,ParameterSetName='JsonRequest')] $Parameter, [Microsoft.PowerShell.Commands.WebRequestMethod]$Method = 'Post' ) $optionalParams = @{ } if( $PSCmdlet.ParameterSetName -eq 'JsonRequest' ) { if( $Parameter ) { $optionalParams['Body'] = $Parameter | ConvertTo-Json Write-WhiskeyVerbose -Context $TaskContext -Message $optionalParams['Body'] } $ContentType = 'application/json' } elseif( $PSCmdlet.ParameterSetName -eq 'FileUpload' ) { $optionalParams['InFile'] = $InFile } try { $ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue Invoke-RestMethod -Uri $Uri -Method $Method -Headers $headers -ContentType $ContentType @optionalParams } catch { if( $ErrorActionPreference -eq 'Ignore' ) { $Global:Error.RemoveAt(0) } Stop-WhiskeyTask -TaskContext $TaskContext -Message ('GitHub API call to "{0}" failed: {1}' -f $uri,$_) return } } $tag = $TaskParameter['Tag'] if( -not $tag ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Tag" is mandatory. It should be the tag to create in your repository for this release. This is usually a version number. We recommend using the `$(WHISKEY_SEMVER2_NO_BUILD_METADATA)` variable to use the version number of the current build.') return } $release = Invoke-GitHubApi -Uri ('{0}/releases/tags/{1}' -f $baseUri,[Uri]::EscapeUriString($tag)) -Method Get -ErrorAction Ignore $createOrEditMethod = [Microsoft.PowerShell.Commands.WebRequestMethod]::Post $actionDescription = 'Creating' $createOrEditUri = '{0}/releases' -f $baseUri if( $release ) { $createOrEditMethod = [Microsoft.PowerShell.Commands.WebRequestMethod]::Patch $actionDescription = 'Updating' $createOrEditUri = $release.url } $releaseName = $TaskParameter['Name'] $releaseNameDesc = '' $releaseData = @{ tag_name = $tag } if( $TaskParameter['Commitish'] ) { $releaseData['target_commitish'] = $TaskParameter['Commitish'] } if( $releaseName ) { $releaseData['name'] = $releaseName $releaseNameDesc = '"{0}" ' -f $releaseName } if( $TaskParameter['Description'] ) { $releaseData['body'] = $TaskParameter['Description'] } Write-WhiskeyInfo -Context $TaskContext -Message ('{0} release {1}with tag "{2}" at commit "{3}".' -f $actionDescription,$releaseNameDesc,$tag,$TaskContext.BuildMetadata.ScmCommitID) $release = Invoke-GitHubApi -Uri $createOrEditUri -Parameter $releaseData -Method $createOrEditMethod $release if( $TaskParameter['Assets'] ) { $existingAssets = Invoke-GitHubApi -Uri $release.assets_url -Method Get $assetIdx = 0 foreach( $asset in $TaskParameter['Assets'] ) { $basePropertyName = 'Assets[{0}]' -f $assetIdx++ $assetPath = $asset['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext ` -PropertyName ('{0}.Path:' -f $basePropertyName) ` -PathType File ` -Mandatory ` -OnlySinglePath if( -not $assetPath ) { continue } $assetName = $asset['Name'] if( -not $assetName ) { $assetName = $assetPath | Split-Path -Leaf } $existingAsset = $existingAssets | Where-Object { $_ -and $_.name -eq $assetName } if( $existingAsset ) { Write-WhiskeyInfo -Context $TaskContext -Message ('Updating asset "{0}" from file "{1}.' -f $assetName,$assetPath) Invoke-GitHubApi -Method Patch -Uri $existingAsset.url -Parameter @{ name = $assetName; label = $assetName } } else { $uri = $release.upload_url -replace '{[^}]+}$' $uri = '{0}?name={1}&label={1}' -f $uri,[Uri]::EscapeDataString($assetName) $contentType = $asset['ContentType'] if( -not $contentType ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName $basePropertyName -Message ('Property "ContentType" is mandatory. It must be the "{0}" file''s media type. For a list of acceptable types, see https://www.iana.org/assignments/media-types/media-types.xhtml.' -f $assetPath) continue } Write-WhiskeyInfo -Context $TaskContext -Message ('Uploading asset "{0}" from file "{1}".' -f $assetName,$assetPath) Invoke-GitHubApi -Method Post -Uri $uri -ContentType $asset['ContentType'] -InFile $assetPath } } } } function Install-Node { [Whiskey.Task('InstallNode')] [Whiskey.RequiresTool('Node', PathParameterName='NodePath')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [String]$Version, [switch]$Force ) if( $Force -or $Version ) { # Skips install if specified version is already installed Install-WhiskeyNode -InstallRootPath $TaskContext.BuildRoot ` -Version $Version ` -OutFileRootPath $TaskContext.OutputDirectory } } function Import-WhiskeyTask { [Whiskey.Task('LoadTask')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')] [String[]]$Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $module = Get-Module -Name 'Whiskey' foreach( $pathItem in $Path ) { $fullPathItem = Resolve-Path -Path $pathItem | Select-Object -ExpandProperty 'ProviderPath' if( $TaskContext.TaskPaths | Where-Object { $_.FullName -eq $fullPathItem } ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Already loaded tasks from file "{0}".' -f $pathItem) continue } $knownTasks = @{} Get-WhiskeyTask | ForEach-Object { $knownTasks[$_.Name] = $_ } # We do this in a background script block to ensure the function is scoped correctly. If it isn't, it # won't be available outside the script block. If it is, it will be visible after the script block completes. & { . $pathItem } $newTasks = Get-WhiskeyTask | Where-Object { -not $knownTasks.ContainsKey($_.Name) } if( -not $newTasks ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('File "{0}" contains no Whiskey tasks. Make sure: * the file contains a function * the function is scoped correctly (e.g. `function script:MyTask`) * the function has a `[Whiskey.Task(''MyTask'')]` attribute that declares the task''s name * a task with the same name hasn''t already been loaded See about_Whiskey_Writing_Tasks for more information.' -f $pathItem) return } Write-WhiskeyInfo -Context $TaskContext -Message ($pathItem) foreach( $task in $newTasks ) { Write-WhiskeyInfo -Context $TaskContext -Message (' {0}' -f $task.Name) } $TaskContext.TaskPaths.Add((Get-Item -Path $pathItem)) } } function Write-WhiskeyLog { [CmdletBinding()] [Whiskey.Task('Log',SupportsClean,SupportsInitialize)] param( [Parameter(Mandatory)] [Whiskey.Context]$Context, [String[]]$Message, [String]$Level = 'Info' ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $infoCmd = Get-Command -Name 'Write-WhiskeyInfo' -ModuleName 'Whiskey' if( -not $infoCmd ) { Stop-WhiskeyTask -Context $Context -Message ('Umm, we can''t seem to find Whiskey''s Write-WhiskeyInfo function. Something pretty bad has gone wrong.') return } $levels = $infoCmd.Parameters.GetEnumerator() | Where-Object { $_.Key -eq 'Level' } | Select-Object -ExpandProperty 'Value' | Select-Object -ExpandProperty 'Attributes' | Where-Object { $_ -is [Management.Automation.ValidateSetAttribute] } | Select-Object -ExpandProperty 'ValidValues' if( -not $levels ) { Stop-WhiskeyTask -Context $Context -Message ('We can''t seem to find the ValidateSet attribute on the Write-WhiskeyInfo function''s Level parameter. Somethign pretty bad has gone wrong.') return } if( $Level -notin $levels ) { Stop-WhiskeyTask -TaskContext $Context -Message ('Property "Level" has an invalid value, "{0}". Valid values are {1}.' -f $Level,($levels -join ", ")) return } Write-WhiskeyInfo -Context $Context -Message $Message -Level $Level } function Merge-WhiskeyFile { [CmdletBinding()] [Whiskey.Task('MergeFile')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')] [String[]]$Path, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File',AllowNonexistent,Create)] [String]$DestinationPath, [switch]$DeleteSourceFiles, [String]$TextSeparator, [Byte[]]$BinarySeparator, [switch]$Clear, [String[]]$Exclude ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $Clear ) { Clear-Content -Path $DestinationPath } if( $TextSeparator -and $BinarySeparator ) { Stop-WhiskeyTask -TaskContext $TaskContext ` -Message ('You can''t use both a text separator and binary separator when merging files. Please use only the TextSeparator or BinarySeparator property, not both.') return } [Byte[]]$separatorBytes = $BinarySeparator if( $TextSeparator ) { $separatorBytes = [Text.Encoding]::UTF8.GetBytes($TextSeparator) } $relativePath = Resolve-Path -Path $DestinationPath -Relative $writer = [IO.File]::OpenWrite($relativePath) try { Write-WhiskeyInfo -Context $TaskContext -Message $relativePath # Move to the end of the file. $writer.Position = $writer.Length # Only add the separator first if we didn't clear the file's original contents. $addSeparator = (-not $Clear) -and ($writer.Length -gt 0) # Normalize the exclusion pattern so it works across platforms. $Exclude = $Exclude | ForEach-Object { $_ -replace '\\|/',[IO.Path]::DirectorySeparatorChar } foreach( $filePath in $Path ) { $excluded = $false foreach( $pattern in $Exclude ) { if( $filePath -like $pattern ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Skipping file "{0}": it matches exclusion pattern "{1}".' -f $filePath,$pattern) $excluded = $true break } else { Write-WhiskeyDebug -Context $TaskContext -Message ('"{0}" -notlike "{1}"' -f $filePath,$pattern) } } if( $excluded ) { continue } $relativePath = Resolve-Path -Path $filePath -Relative Write-WhiskeyInfo -Context $TaskContext -Message (' + {0}' -f $relativePath) if( $addSeparator -and $separatorBytes ) { $writer.Write($separatorBytes,0,$separatorBytes.Length) } $addSeparator = $true $reader = [IO.File]::OpenRead($filePath) try { $bufferSize = 4kb [Byte[]]$buffer = New-Object 'byte[]' ($bufferSize) while( $bytesRead = $reader.Read($buffer,0,$bufferSize) ) { $writer.Write($buffer,0,$bytesRead) } } finally { $reader.Close() } if( $DeleteSourceFiles ) { Remove-Item -Path $filePath -Force } } } finally { $writer.Close() } } function Invoke-WhiskeyMSBuild { [Whiskey.Task('MSBuild', SupportsClean, Platform='Windows')] [Whiskey.RequiresPowerShellModule('VSSetup', Version='2.*', VersionParameterName='VSSetupVersion')] [Whiskey.RequiresNuGetPackage('NuGet.CommandLine', Version='6.3.1', VersionParameterName='NuGetVersion', PathParameterName='NuGetPath')] [CmdletBinding()] param( [Whiskey.Context]$TaskContext, [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')] [String[]]$Path, [Whiskey.Tasks.ValidatePath(AllowNonexistent,PathType='Directory',Create)] [String]$OutputDirectory, [String] $NuGetPath ) Set-StrictMode -version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState #setup $msbuildInfos = Get-MSBuild | Sort-Object -Descending 'Version' $version = $TaskParameter['Version'] if( $version ) { $msbuildInfo = $msbuildInfos | Where-Object { $_.Name -eq $version } | Select-Object -First 1 } else { $msbuildInfo = $msbuildInfos | Select-Object -First 1 } if( -not $msbuildInfo ) { $msbuildVersionNumbers = $msbuildInfos | Select-Object -ExpandProperty 'Name' Stop-WhiskeyTask -TaskContext $TaskContext -Message ('MSBuild {0} is not installed. Installed versions are: {1}' -f $version,($msbuildVersionNumbers -join ', ')) return } $msbuildExePath = $msbuildInfo.Path if( $TaskParameter.ContainsKey('Use32Bit') -and ($TaskParameter['Use32Bit'] | ConvertFrom-WhiskeyYamlScalar) ) { $msbuildExePath = $msbuildInfo.Path32 if( -not $msbuildExePath ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('A 32-bit version of MSBuild {0} does not exist.' -f $version) return } } Write-WhiskeyVerbose -Context $TaskContext -Message ('{0}' -f $msbuildExePath) $target = @( 'build' ) if( $TaskContext.ShouldClean ) { $target = 'clean' } else { if( $TaskParameter.ContainsKey('Target') ) { $target = $TaskParameter['Target'] } } $NuGetPath = Join-Path -Path $NuGetPath -ChildPath 'tools\NuGet.exe' -Resolve if( -not $NuGetPath ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "NuGet.exe not found at ""$($nugetPath)""." return } foreach( $projectPath in $Path ) { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $projectPath) if( $projectPath -like '*.sln' ) { if( $TaskContext.ShouldClean ) { $packageDirectoryPath = Join-Path -Path ( Split-Path -Path $projectPath -Parent ) -ChildPath 'packages' if( Test-Path -Path $packageDirectoryPath -PathType Container ) { Write-WhiskeyVerbose -Context $TaskContext -Message (' Removing NuGet packages at {0}.' -f $packageDirectoryPath) Remove-Item $packageDirectoryPath -Recurse -Force } } else { Write-WhiskeyCommand -Path $NuGetPath -ArgumentList 'restore', $projectPath & $NuGetPath restore $projectPath } } if( $TaskContext.ByBuildServer ) { $projectPath | Split-Path | Get-ChildItem -Filter 'AssemblyInfo.cs' -Recurse | ForEach-Object { $assemblyInfo = $_ $assemblyInfoPath = $assemblyInfo.FullName $newContent = Get-Content -Path $assemblyInfoPath | Where-Object { $_ -notmatch '\bAssembly(File|Informational)?Version\b' } $newContent | Set-Content -Path $assemblyInfoPath Write-WhiskeyVerbose -Context $TaskContext -Message (' Updating version in {0}.' -f $assemblyInfoPath) @" [assembly: System.Reflection.AssemblyVersion("{0}")] [assembly: System.Reflection.AssemblyFileVersion("{0}")] [assembly: System.Reflection.AssemblyInformationalVersion("{1}")] "@ -f $TaskContext.Version.Version,$TaskContext.Version.SemVer2 | Add-Content -Path $assemblyInfoPath } } $verbosity = 'm' if( $TaskParameter['Verbosity'] ) { $verbosity = $TaskParameter['Verbosity'] } $configuration = Get-WhiskeyMSBuildConfiguration -Context $TaskContext $property = Invoke-Command { Write-Output ('Configuration={0}' -f $configuration) if( $TaskParameter.ContainsKey('Property') ) { Write-Output ($TaskParameter['Property']) } if( $OutputDirectory ) { # Get an absolute path. MSBuild interprets relative paths as being relative to .csproj being compiled. $OutputDirectory = Resolve-Path -Path $OutputDirectory | Select-Object -ExpandProperty 'ProviderPath' Write-Output ('OutDir={0}' -f $OutputDirectory) } } $cpuArg = '/maxcpucount' $cpuCount = $TaskParameter['CpuCount'] | ConvertFrom-WhiskeyYamlScalar if( $cpuCount ) { $cpuArg = '/maxcpucount:{0}' -f $TaskParameter['CpuCount'] } if( ($TaskParameter['NoMaxCpuCountArgument'] | ConvertFrom-WhiskeyYamlScalar) ) { $cpuArg = '' } $noFileLogger = $TaskParameter['NoFileLogger'] | ConvertFrom-WhiskeyYamlScalar $projectFileName = $projectPath | Split-Path -Leaf $logFilePath = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('msbuild.{0}.log' -f $projectFileName) $msbuildArgs = Invoke-Command { ('/verbosity:{0}' -f $verbosity) $cpuArg $TaskParameter['Argument'] if( -not $noFileLogger ) { '/filelogger9' ('/flp9:LogFile={0};Verbosity=d' -f $logFilePath) } } | Where-Object { $_ } $separator = '{0}VERBOSE: ' -f [Environment]::NewLine Write-WhiskeyVerbose -Context $TaskContext -Message (' Target {0}' -f ($target -join $separator)) Write-WhiskeyVerbose -Context $TaskContext -Message (' Property {0}' -f ($property -join $separator)) Write-WhiskeyVerbose -Context $TaskContext -Message (' Argument {0}' -f ($msbuildArgs -join $separator)) $propertyArgs = & { if ($property) { Write-WhiskeyVerbose "Escaping MSBuild property values." } foreach ($item in $property) { $name,$value = $item -split '=',2 # Unescape first in case the there's already an escaped character in there. $value = [Uri]::UnescapeDataString($value) $value = [Uri]::EscapeDataString($value) Write-WhiskeyVerbose " ${item} -> ${name}=${value}" "/p:${name}=${value}" } } $targetArg = '/t:{0}' -f ($target -join ';') Write-WhiskeyCommand -Path $msbuildExepath ` -ArgumentList (& { $projectPath ; $targetArg ; $propertyArgs ; $msbuildArgs }) & $msbuildExePath $projectPath $targetArg $propertyArgs $msbuildArgs /nologo if( $LASTEXITCODE -ne 0 ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('MSBuild exited with code {0}.' -f $LASTEXITCODE) return } } } function Invoke-WhiskeyNodeTask { [Whiskey.Task('Node',SupportsClean,SupportsInitialize,Obsolete,ObsoleteMessage='The "Node" task is obsolete and will be removed in a future version of Whiskey. It''s functionality has been broken up into the "Npm" and "NodeLicenseChecker" tasks.')] [Whiskey.RequiresTool('Node',PathParameterName='NodePath')] [Whiskey.RequiresNodeModule('license-checker', PathParameterName='LicenseCheckerPath', VersionParameterName='LicenseCheckerVersion')] [Whiskey.RequiresNodeModule('nsp', PathParameterName='NspPath', VersionParameterName='PINNED_TO_NSP_2_7_0', Version='2.7.0')] [CmdletBinding()] param( [Parameter(Mandatory)] # The context the task is running under. [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] # The task parameters, which are: # # * `NpmScript`: a list of one or more NPM scripts to run, e.g. `npm run $SCRIPT_NAME`. Each script is run indepently. # * `WorkingDirectory`: the directory where all the build commands should be run. Defaults to the directory where the build's `whiskey.yml` file was found. Must be relative to the `whiskey.yml` file. [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $TaskContext.ShouldClean ) { Write-WhiskeyDebug -Context $TaskContext -Message 'Cleaning' $nodeModulesPath = Join-Path -Path $TaskContext.BuildRoot -ChildPath 'node_modules' Remove-WhiskeyFileSystemItem -Path $nodeModulesPath Write-WhiskeyDebug -Context $TaskContext -Message 'COMPLETE' return } $npmScripts = $TaskParameter['NpmScript'] $npmScriptCount = $npmScripts | Measure-Object | Select-Object -ExpandProperty 'Count' $numSteps = 4 + $npmScriptCount $stepNum = 0 $activity = 'Running Node Task' function Update-Progress { param( [Parameter(Mandatory)] [String]$Status, [int]$Step ) Write-Progress -Activity $activity -Status $Status.TrimEnd('.') -PercentComplete ($Step/$numSteps*100) } $workingDirectory = (Get-Location).ProviderPath $originalPath = $env:PATH try { $nodePath = Resolve-WhiskeyNodePath -BuildRoot $TaskContext.BuildRoot Set-Item -Path 'env:PATH' -Value ('{0}{1}{2}' -f ($nodePath | Split-Path),[IO.Path]::PathSeparator,$env:PATH) Update-Progress -Status ('Installing NPM packages') -Step ($stepNum++) Write-WhiskeyDebug -Context $TaskContext -Message ('npm install') Invoke-WhiskeyNpmCommand -Name 'install' -ArgumentList '--production=false' -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop Write-WhiskeyDebug -Context $TaskContext -Message ('COMPLETE') if( $TaskContext.ShouldInitialize ) { Write-WhiskeyDebug -Context $TaskContext -Message 'Initialization complete.' return } if( -not $npmScripts ) { Write-WhiskeyWarning -Context $TaskContext -Message (@' Property 'NpmScript' is missing or empty. Your build isn''t *doing* anything. The 'NpmScript' property should be a list of one or more npm scripts to run during your build, e.g. Build: - Node: NpmScript: - build - test '@) } foreach( $script in $npmScripts ) { Update-Progress -Status ('npm run {0}' -f $script) -Step ($stepNum++) Write-WhiskeyDebug -Context $TaskContext -Message ('Running script ''{0}''.' -f $script) Invoke-WhiskeyNpmCommand -Name 'run-script' -ArgumentList $script -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop Write-WhiskeyDebug -Context $TaskContext -Message ('COMPLETE') } $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $TaskContext.BuildRoot Update-Progress -Status ('nsp check') -Step ($stepNum++) Write-WhiskeyDebug -Context $TaskContext -Message ('Running NSP security check.') $nspPath = Assert-WhiskeyNodeModulePath -Path $TaskParameter['NspPath'] -CommandPath 'bin\nsp' -ErrorAction Stop $output = & $nodePath $nspPath 'check' '--output' 'json' 2>&1 | ForEach-Object { if( $_ -is [Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } Write-WhiskeyDebug -Context $TaskContext -Message ('COMPLETE') $results = ($output -join [Environment]::NewLine) | ConvertFrom-Json if( $LASTEXITCODE ) { $summary = $results | Format-List | Out-String Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NSP, the Node Security Platform, found the following security vulnerabilities in your dependencies (exit code: {0}):{1}{2}' -f $LASTEXITCODE,[Environment]::NewLine,$summary) return } Update-Progress -Status ('license-checker') -Step ($stepNum++) Write-WhiskeyDebug -Context $TaskContext -Message ('Generating license report.') $licenseCheckerPath = Assert-WhiskeyNodeModulePath -Path $TaskParameter['LicenseCheckerPath'] -CommandPath 'bin\license-checker' -ErrorAction Stop $reportJson = & $nodePath $licenseCheckerPath '--json' Write-WhiskeyDebug -Context $TaskContext -Message ('COMPLETE') $report = ($reportJson -join [Environment]::NewLine) | ConvertFrom-Json if( -not $report ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('License Checker failed to output a valid JSON report.') return } Write-WhiskeyDebug -Context $TaskContext -Message ('Converting license report.') # The default license checker report has a crazy format. It is an object with properties for each module. # Let's transform it to a more sane format: an array of objects. [Object[]]$newReport = $report | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty 'Name' | ForEach-Object { $report.$_ | Add-Member -MemberType NoteProperty -Name 'name' -Value $_ -PassThru } # show the report $newReport | Sort-Object -Property 'licenses','name' | Format-Table -Property 'licenses','name' -AutoSize | Out-String | Write-WhiskeyVerbose -Context $TaskContext $licensePath = 'node-license-checker-report.json' $licensePath = Join-Path -Path $TaskContext.OutputDirectory -ChildPath $licensePath ConvertTo-Json -InputObject $newReport -Depth 100 | Set-Content -Path $licensePath Write-WhiskeyDebug -Context $TaskContext -Message ('COMPLETE') } finally { Set-Item -Path 'env:PATH' -Value $originalPath Write-Progress -Activity $activity -Completed -PercentComplete 100 } } function Invoke-WhiskeyNodeLicenseChecker { [CmdletBinding()] [Whiskey.Task('NodeLicenseChecker')] [Whiskey.RequiresTool('Node', PathParameterName='NodePath', VersionParameterName='NodeVersion')] [Whiskey.RequiresNodeModule('license-checker', PathParameterName='LicenseCheckerPath')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [String[]]$Arguments ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $licenseCheckerPath = Assert-WhiskeyNodeModulePath -Path $TaskParameter['LicenseCheckerPath'] -CommandPath 'bin\license-checker' -ErrorAction Stop $nodePath = Assert-WhiskeyNodePath -Path $TaskParameter['NodePath'] -ErrorAction Stop Write-WhiskeyDebug -Context $TaskContext -Message ('Generating license report') Invoke-Command -NoNewScope -ScriptBlock { & $nodePath $licenseCheckerPath $Arguments } if( $LASTEXITCODE -eq 1 ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "license-checker returned a non-zero exit code. See above output for more details." return } Write-WhiskeyDebug -Context $TaskContext -Message ('COMPLETE') } function Invoke-WhiskeyNodeNspCheck { [Whiskey.Task('NodeNspCheck', Obsolete, ObsoleteMessage='The "NodeNspCheck" task is obsolete and will be removed in a future version of Whiskey. Please use the "Npm" task instead. The NSP project shut down in September 2018 and was replaced with the `npm audit` command.')] [Whiskey.RequiresTool('Node', PathParameterName='NodePath', VersionParameterName='NodeVersion')] [Whiskey.RequiresNodeModule('nsp', PathParameterName='NspPath')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $nspPath = Assert-WhiskeyNodeModulePath -Path $TaskParameter['NspPath'] -CommandPath 'bin\nsp' -ErrorAction Stop $nodePath = Assert-WhiskeyNodePath -Path $TaskParameter['NodePath'] -ErrorAction Stop $formattingArg = '--reporter' $isPreNsp3 = $TaskParameter.ContainsKey('Version') -and $TaskParameter['Version'] -match '^(0|1|2)\.' if( $isPreNsp3 ) { $formattingArg = '--output' } Write-WhiskeyDebug -Context $TaskContext -Message 'Running NSP security check' $output = Invoke-Command -NoNewScope -ScriptBlock { param( $JsonOutputFormat ) & $nodePath $nspPath 'check' $JsonOutputFormat 'json' 2>&1 | ForEach-Object { if( $_ -is [Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } } -ArgumentList $formattingArg Write-WhiskeyDebug -Context $TaskContext -Message 'COMPLETE' try { $results = ($output -join [Environment]::NewLine) | ConvertFrom-Json } catch { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NSP, the Node Security Platform, did not run successfully as it did not return valid JSON (exit code: {0}):{1}{2}' -f $LASTEXITCODE,[Environment]::NewLine,$output) return } if ($Global:LASTEXITCODE -ne 0) { $summary = $results | Format-List | Out-String Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NSP, the Node Security Platform, found the following security vulnerabilities in your dependencies (exit code: {0}):{1}{2}' -f $LASTEXITCODE,[Environment]::NewLine,$summary) return } } function Invoke-WhiskeyNpm { [Whiskey.Task('Npm')] [Whiskey.RequiresTool('Node',PathParameterName='NodePath',VersionParameterName='NodeVersion')] [Whiskey.RequiresNodeModule('npm', PathParameterName='NpmPath', VersionParameterName='NpmVersion')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $commandName = $TaskParameter['Command'] if( -not $commandName ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Command" is required. It should be the name of the NPM command to run. See https://docs.npmjs.com/cli#cli for a list.') return } Invoke-WhiskeyNpmCommand -Name $commandName -BuildRootPath $TaskContext.BuildRoot -ArgumentList $TaskParameter['Argument'] -ErrorAction Stop } function Invoke-WhiskeyNpmConfig { [Whiskey.Task('NpmConfig',Obsolete,ObsoleteMessage='The "NpmConfig" task is obsolete. It will be removed in a future version of Whiskey. Please use the "Npm" task instead.')] [Whiskey.RequiresTool('Node',PathParameterName='NodePath',VersionParameterName='NodeVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $configuration = $TaskParameter['Configuration'] if( -not $configuration ) { Write-WhiskeyWarning -Context $TaskContext -Message ('Your NpmConfig task isn''t doing anything. Its Configuration property is missing. Please update the NpmConfig task in your whiskey.yml file so that it is actually setting configuration, e.g. Build: - NpmConfig: Configuration: key1: value1 key2: value2 ') return } if( -not ($configuration | Get-Member -Name 'Keys') ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Configuration property is invalid. It must have only key/value pairs, e.g. Build: - NpmConfig: Configuration: key1: value1 key2: value2 ') return } $scope = $TaskParameter['Scope'] if( $scope ) { if( @('Project', 'User', 'Global') -notcontains $scope ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Scope property ''{0}'' is invalid. Allowed values are `Project`, `User`, `Global` to set configuration at the project, user, or global level. You may also remove the `Scope` property to set configuration at the project level (i.e. in the current directory).' -f $scope) return } } foreach( $key in $TaskParameter['Configuration'].Keys ) { $argumentList = & { 'set' $key $configuration[$key] if( $scope -eq 'User' ) { } elseif( $scope -eq 'Global' ) { '-g' } else { '-userconfig' '.npmrc' } } Invoke-WhiskeyNpmCommand -Name 'config' -ArgumentList $argumentList -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper } } function Invoke-WhiskeyNpmInstall { [Whiskey.Task('NpmInstall',SupportsClean,Obsolete,ObsoleteMessage='The "NpmInstall" task is obsolete. It will be removed in a future version of Whiskey. Please use the "Npm" task instead.')] [Whiskey.RequiresTool('Node',PathParameterName='NodePath',VersionParameterName='NodeVersion')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $workingDirectory = (Get-Location).ProviderPath if( -not $TaskParameter['Package'] ) { if( $TaskContext.ShouldClean ) { Write-WhiskeyDebug -Context $TaskContext -Message 'Removing project node_modules' Remove-WhiskeyFileSystemItem -Path 'node_modules' -ErrorAction Stop } else { Write-WhiskeyDebug -Context $TaskContext -Message 'Installing Node modules' Invoke-WhiskeyNpmCommand -Name 'install' -ArgumentList '--production=false' -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop } Write-WhiskeyDebug -Context $TaskContext -Message 'COMPLETE' } else { $installGlobally = $false if( $TaskParameter.ContainsKey('Global') ) { $installGlobally = $TaskParameter['Global'] | ConvertFrom-WhiskeyYamlScalar } foreach( $package in $TaskParameter['Package'] ) { $packageVersion = '' if ($package | Get-Member -Name 'Keys') { $packageName = $package.Keys | Select-Object -First 1 $packageVersion = $package[$packageName] } else { $packageName = $package } if( $TaskContext.ShouldClean ) { if( $TaskParameter.ContainsKey('NodePath') -and (Test-Path -Path $TaskParameter['NodePath'] -PathType Leaf) ) { Write-WhiskeyDebug -Context $TaskContext -Message ('Uninstalling {0}' -f $packageName) Uninstall-WhiskeyNodeModule -BuildRootPath $TaskContext.BuildRoot ` -Name $packageName ` -ForDeveloper:$TaskContext.ByDeveloper ` -Global:$installGlobally ` -ErrorAction Stop } } else { Write-WhiskeyDebug -Context $TaskContext -Message ('Installing {0}' -f $packageName) Install-WhiskeyNodeModule -BuildRootPath $TaskContext.BuildRoot ` -Name $packageName ` -Version $packageVersion ` -ForDeveloper:$TaskContext.ByDeveloper ` -Global:$installGlobally ` -ErrorAction Stop } Write-WhiskeyDebug -Context $TaskContext -Message 'COMPLETE' } } } function Invoke-WhiskeyNpmPrune { [Whiskey.Task('NpmPrune',Obsolete,ObsoleteMessage='The "NpmPrune" task is obsolete. It will be removed in a future version of Whiskey. Please use the "Npm" task instead.')] [Whiskey.RequiresTool('Node',PathParameterName='NodePath',VersionParameterName='NodeVersion')] [CmdletBinding()] param( [Parameter(Mandatory)] # The context the task is running under. [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] # The parameters/configuration to use to run the task. [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Invoke-WhiskeyNpmCommand -Name 'prune' -ArgumentList '--production' -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop } function Invoke-WhiskeyNpmRunScript { [Whiskey.Task('NpmRunScript',Obsolete,ObsoleteMessage='The "NpmRunScriptTask" is obsolete. It will be removed in a future version of Whiskey. Please use the "Npm" task instead.')] [Whiskey.RequiresTool('Node',PathParameterName='NodePath',VersionParameterName='NodeVersion')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $npmScripts = $TaskParameter['Script'] if (-not $npmScripts) { Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property ''Script'' is mandatory. It should be a list of one or more npm scripts to run during your build, e.g., Build: - NpmRunScript: Script: - build - test ' return } foreach ($script in $npmScripts) { Write-WhiskeyDebug -Context $TaskContext -Message ('Running script ''{0}''.' -f $script) Invoke-WhiskeyNpmCommand -Name 'run-script' -ArgumentList $script -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop Write-WhiskeyDebug -Context $TaskContext -Message ('COMPLETE') } } function New-WhiskeyNuGetPackage { [Whiskey.Task('NuGetPack',Platform='Windows')] [Whiskey.RequiresNuGetPackage('NuGet.CommandLine', Version='6.*', PathParameterName='NuGetPath')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')] [String[]]$Path, [String] $NuGetPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $symbols = $TaskParameter['Symbols'] | ConvertFrom-WhiskeyYamlScalar $symbolsArg = $null $symbolsFileNameSuffix = '' if ($symbols) { $symbolsArg = '-Symbols' $symbolsFileNameSuffix = '.symbols' } $NuGetPath = Join-Path -Path $NuGetPath -ChildPath 'tools\NuGet.exe' -Resolve if( -not $NuGetPath ) { Stop-WhiskeyTask -Context $TaskContext -Message "NuGet.exe not found at ""$($nugetPath)""." return } $properties = $TaskParameter['Properties'] $propertiesArgs = @() if( $properties ) { if( -not (Get-Member -InputObject $properties -Name 'Keys') ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Properties' -Message ('Property is invalid. This property must be a name/value mapping of properties to pass to nuget.exe pack command''s "-Properties" parameter.') return } $propertiesArgs = $properties.Keys | ForEach-Object { '-Properties' '{0}={1}' -f $_,$properties[$_] } } foreach ($pathItem in $Path) { $projectName = $TaskParameter['PackageID'] if( -not $projectName ) { $projectName = [IO.Path]::GetFileNameWithoutExtension(($pathItem | Split-Path -Leaf)) } $packageVersion = $TaskParameter['PackageVersion'] if (-not $packageVersion) { $packageVersion = $TaskContext.Version.SemVer1 } # Create NuGet package $configuration = Get-WhiskeyMSBuildConfiguration -Context $TaskContext $configPropertyArg = "Configuration=${configuration}" Write-WhiskeyCommand -Path $NuGetPath ` -ArgumentList @( 'pack', '-Version', $packageVersion, '-OutputDirectory', $TaskContext.OutputDirectory, $symbolsArg, $configPropertyArg, $propertiesArgs, $pathItem ) & $nugetPath pack ` -Version $packageVersion ` -OutputDirectory $TaskContext.OutputDirectory ` $symbolsArg ` -Properties ('Configuration={0}' -f $configuration) ` $propertiesArgs ` $pathItem # Make sure package was created. $filename = '{0}.{1}{2}.nupkg' -f $projectName,$packageVersion,$symbolsFileNameSuffix $packagePath = Join-Path -Path $TaskContext.OutputDirectory -childPath $filename if( -not (Test-Path -Path $packagePath -PathType Leaf) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('We ran nuget pack against "{0}" but the expected NuGet package "{1}" does not exist.' -f $pathItem,$packagePath) return } } } function Publish-WhiskeyNuGetPackage { [Whiskey.Task('NuGetPush', Platform='Windows', Aliases=('PublishNuGetLibrary','PublishNuGetPackage'), WarnWhenUsingAlias)] [Whiskey.RequiresNuGetPackage('NuGet.CommandLine', Version='6.*', PathParameterName='NuGetPath')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(PathType='File')] [String[]]$Path, [String] $NuGetPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState if( -not $Path ) { $Path = Join-Path -Path $TaskContext.OutputDirectory.FullName -ChildPath '*.nupkg' | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PathType 'File' -PropertyName 'Path' } $publishSymbols = $TaskParameter['Symbols'] | ConvertFrom-WhiskeyYamlScalar $paths = $Path | Where-Object { $wildcard = '*.symbols.nupkg' if( $publishSymbols ) { $_ -like $wildcard } else { $_ -notlike $wildcard } } $source = $TaskParameter['Uri'] if( -not $source ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''Uri'' is mandatory. It should be the URI where NuGet packages should be published, e.g. Build: - PublishNuGetPackage: Uri: https://nuget.org ') return } $apiKeyID = $TaskParameter['ApiKeyID'] if( -not $apiKeyID ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''ApiKeyID'' is mandatory. It should be the ID/name of the API key to use when publishing NuGet packages to {0}, e.g.: Build: - PublishNuGetPackage: Uri: {0} ApiKeyID: API_KEY_ID Use the `Add-WhiskeyApiKey` function to add the API key to the build. ' -f $source) return } $apiKey = Get-WhiskeyApiKey -Context $TaskContext -ID $apiKeyID -PropertyName 'ApiKeyID' $NuGetPath = Join-Path -Path $NuGetPath -ChildPath 'tools\NuGet.exe' -Resolve if( -not $NuGetPath ) { Stop-WhiskeyTask -Context $TaskContext -Message "NuGet.exe not found at ""$($NuGetPath)""." return } foreach ($packagePath in $paths) { $packageFilename = [IO.Path]::GetFileNameWithoutExtension(($packagePath | Split-Path -Leaf)) $packageName = $packageFilename -replace '\.\d+\.\d+\.\d+(-.*)?(\.symbols)?','' $packageFilename -match '(\d+\.\d+\.\d+(?:-[0-9a-z]+)?)' $packageVersion = $Matches[1] $packageUri = '{0}/package/{1}/{2}' -f $source,$packageName,$packageVersion # Make sure this version doesn't exist. $packageExists = $false $numErrorsAtStart = $Global:Error.Count try { $ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue Invoke-WebRequest -Uri $packageUri -UseBasicParsing | Out-Null $packageExists = $true } catch { # Invoke-WebRequest throws differnt types of errors in Windows PowerShell and PowerShell Core. Handle the case where a non-HTTP exception occurs. if( -not ($_.Exception | Get-Member 'Response') ) { Write-Error -ErrorRecord $_ $msg = "Unknown failure checking if $($packageName) $($packageVersion) package already exists at " + "$($packageUri): $($_)" Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $response = $_.Exception.Response if( -not ($response | Get-Member 'StatusCode') ) { Write-Error -ErrorRecord $_ $msg = "Unable to determine HTTP status code from failed HTTP response to $($packageUri) checking if " + "$($packageName) $($packageVersion) exists: $($_)" Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } if( $response.StatusCode -ne [Net.HttpStatusCode]::NotFound ) { $content = '' if( $response | Get-Member 'GetResponseStream' ) { $responseStream = $response.GetResponseStream() $responseStream.Position = 0 $reader = New-Object 'IO.StreamReader' $responseStream $content = $reader.ReadToEnd() -replace '<[^>]+?>','' $reader.Close() $response.Close() } $msg = "Failure checking if $($packageName) $($packageVersion) package already exists at " + "$($packageUri). The web request returned status code $($response.StatusCode) " + "($([int]$response.StatusCode)) status code: $($content)" Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } for( $idx = 0; $idx -lt ($Global:Error.Count - $numErrorsAtStart); ++$idx ) { $Global:Error.RemoveAt(0) } } if( $packageExists ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('{0} {1} already exists. Please increment your library''s version number in ''{2}''.' -f $packageName,$packageVersion,$TaskContext.ConfigurationPath) return } # Publish package and symbols to NuGet Invoke-WhiskeyNuGetPush -Path $packagePath -Url $source -ApiKey $apiKey -NuGetPath $NuGetPath if( -not ($TaskParameter['SkipUploadedCheck'] | ConvertFrom-WhiskeyYamlScalar) ) { try { $ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue Invoke-WebRequest -Uri $packageUri -UseBasicParsing | Out-Null } catch { # Invoke-WebRequest throws differnt types of errors in Windows PowerShell and PowerShell Core. Handle the case where a non-HTTP exception occurs. if( -not ($_.Exception | Get-Member 'Response') ) { Write-Error -ErrorRecord $_ Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unknown failure checking if {0} {1} package was published to {2}. {3}' -f $packageName,$packageVersion,$packageUri,$_) return } Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failed to publish NuGet package {0} {1} to {2}. When we checked if that package existed, we got a {3} HTTP status code. Please see build output for more information.' -f $packageName,$packageVersion,$packageUri,$_.Exception.Response.StatusCode) return } } } } function Restore-WhiskeyNuGetPackage { [CmdletBinding()] [Whiskey.TaskAttribute('NuGetRestore', Platform='Windows')] [Whiskey.RequiresNuGetPackage('NuGet.CommandLine', Version='6.*', PathParameterName='NuGetPath')] param( [Parameter(Mandatory)] [Whiskey.Tasks.ValidatePath(Mandatory)] [String[]] $Path, [String[]] $Argument, [String] $Version, [String] $NuGetPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $NuGetPath = Join-Path -Path $NuGetPath -ChildPath 'tools\NuGet.exe' -Resolve if( -not $NuGetPath ) { Stop-WhiskeyTask -Context $TaskContext -Message "NuGet.exe not found at ""$($NuGetPath)""." return } foreach( $item in $Path ) { & $nuGetPath 'restore' $item $Argument } } function Invoke-WhiskeyNUnit2Task { [Whiskey.Task('NUnit2', Platform='Windows')] [Whiskey.RequiresNuGetPackage('NUnit.Runners', Version='2.*', PathParameterName='NUnitPath')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory)] [hashtable] $TaskParameter, # TODO: Once this task uses NuGet tool provider, make this Mandatory and remove the test that Path has a value. [Whiskey.Tasks.ValidatePath(AllowNonexistent, PathType='File')] [String[]]$Path, [String] $NUnitPath ) Set-StrictMode -version 'latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $includeParam = $null if( $TaskParameter.ContainsKey('Include') ) { $includeParam = '/include={0}' -f $TaskParameter['Include'].Trim('"') } $excludeParam = $null if( $TaskParameter.ContainsKey('Exclude') ) { $excludeParam = '/exclude={0}' -f $TaskParameter['Exclude'].Trim('"') } $frameworkParam = '4.0' if( $TaskParameter.ContainsKey('Framework') ) { $frameworkParam = $TaskParameter['Framework'] } $frameworkParam = '/framework={0}' -f $frameworkParam $nunitToolsRoot = Join-Path -Path $NUnitPath -ChildPath 'tools' $nunitConsolePath = Join-Path -Path $nunitToolsRoot -ChildPath 'nunit-console.exe' if( -not (Test-Path -Path $nunitConsolePath) ) { $msg = "NUnit doesn't exist at ""$($nunitConsolePath)""." Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } if( -not $Path ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Path" is mandatory. It should be one or more paths, which should be a list of assemblies whose tests to run, e.g. Build: - NUnit2: Path: - Assembly.dll - OtherAssembly.dll') return } $missingPaths = $Path | Where-Object { -not (Test-Path -Path $_ -PathType Leaf) } if( $missingPaths ) { $missingPaths = $missingPaths -join ('{0}*' -f [Environment]::NewLine) Stop-WhiskeyTask -TaskContext $TaskContext -Message ('The following paths do not exist.{0} {0}*{1}{0} ' -f [Environment]::NewLine,$missingPaths) return } $reportPath = Join-Path -Path ($TaskContext.OutputDirectory | Resolve-Path -Relative) ` -ChildPath ('nunit2+{0}.xml' -f [IO.Path]::GetRandomFileName()) $extraArgs = $TaskParameter['Argument'] | Where-Object { $_ } Write-WhiskeyVerbose -Context $TaskContext -Message (' Path {0}' -f ($Path | Select-Object -First 1)) $Path | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) } Write-WhiskeyVerbose -Context $TaskContext -Message (' Framework {0}' -f $frameworkParam) Write-WhiskeyVerbose -Context $TaskContext -Message (' Include {0}' -f $includeParam) Write-WhiskeyVerbose -Context $TaskContext -Message (' Exclude {0}' -f $excludeParam) Write-WhiskeyVerbose -Context $TaskContext -Message (' Argument /xml={0}' -f $reportPath) $extraArgs | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) } Write-WhiskeyDebug -Context $TaskContext -Message ('Running NUnit') Write-WhiskeyCommand -Path $nunitConsolePath ` -ArgumentList $Path,$frameworkParam,$includeParam,$excludeParam,$extraArgs,"/xml=${reportPath}" & $nunitConsolePath $Path $frameworkParam $includeParam $excludeParam $extraArgs ('/xml={0}' -f $reportPath) Write-WhiskeyVerbose -Message "$($nunitConsolePath | Resolve-Path -Relative) exited with code $($LastExitCode)." if( $LastExitCode ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NUnit2 tests failed. {0} returned exit code {1}.' -f $nunitConsolePath,$LastExitCode) return } } function Invoke-WhiskeyNUnit3Task { [CmdletBinding()] [Whiskey.Task('NUnit3', Platform='Windows')] [Whiskey.RequiresNuGetPackage('NUnit.Console', Version='3.*')] [Whiskey.RequiresNuGetPackage('NUnit.ConsoleRunner', Version='3.*', PathParameterName='NUnitPath')] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory)] [hashtable] $TaskParameter, # TODO: Once this task uses NuGet tool provider, make this Mandatory and remove the test that Path has a value. [Whiskey.Tasks.ValidatePath(AllowNonexistent, PathType='File')] [String[]] $Path, [String] $NUnitPath, [String] $OpenCoverPath, [String] $ReportGeneratorPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $reportFormat = 'nunit3'; if ($TaskParameter['ResultFormat']) { $reportFormat = $TaskParameter['ResultFormat'] } # NUnit3 currently allows 'nunit2' and 'nunit3' which aligns with output filename usage $nunitReport = Join-Path -Path ($TaskContext.OutputDirectory | Resolve-Path -Relative) ` -ChildPath ('{0}+{1}.xml' -f $reportFormat, [IO.Path]::GetRandomFileName()) $nunitReportParam = '--result={0};format={1}' -f $nunitReport, $reportFormat $framework = 'net-4.0' if ($TaskParameter['Framework']) { $framework = $TaskParameter['Framework'] } $frameworkParam = '--framework={0}' -f $framework $testFilter = '' $testFilterParam = $null if ($TaskParameter['TestFilter']) { $testFilter = $TaskParameter['TestFilter'] | ForEach-Object { '({0})' -f $_ } $testFilter = $testFilter -join ' or ' $testFilterParam = '--where={0}' -f $testFilter } $nunitExtraArgument = $null if ($TaskParameter['Argument']) { $nunitExtraArgument = $TaskParameter['Argument'] } $nunitConsolePath = Get-ChildItem -Path $nunitPath -Filter 'nunit3-console.exe' -Recurse | Select-Object -First 1 | Select-Object -ExpandProperty 'FullName' if( -not $nunitConsolePath ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find "nunit3-console.exe" in NUnit3 NuGet package at "{0}".' -f $nunitPath) return } if( -not $Path ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Path" is mandatory. It should be one or more paths to the assemblies whose tests should be run, e.g. Build: - NUnit3: Path: - Assembly.dll - OtherAssembly.dll ') return } foreach( $pathItem in $Path ) { if (-not (Test-Path -Path $pathItem -PathType Leaf)) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('"Path" item "{0}" does not exist.' -f $pathItem) return } } $separator = '{0}VERBOSE: ' -f [Environment]::NewLine Write-WhiskeyVerbose -Context $TaskContext -Message (' Path {0}' -f ($Path -join $separator)) Write-WhiskeyVerbose -Context $TaskContext -Message (' Framework {0}' -f $framework) Write-WhiskeyVerbose -Context $TaskContext -Message (' TestFilter {0}' -f $testFilter) Write-WhiskeyVerbose -Context $TaskContext -Message (' Argument {0}' -f ($nunitExtraArgument -join $separator)) Write-WhiskeyVerbose -Context $TaskContext -Message (' NUnit Report {0}' -f $nunitReport) $nunitExitCode = 0 Write-WhiskeyCommand -Path $nunitConsolePath ` -ArgumentList $Path, $frameworkParam, $testFilterParam, $nunitReportParam, $nunitExtraArgument & $nunitConsolePath $Path $frameworkParam $testFilterParam $nunitReportParam $nunitExtraArgument $nunitExitCode = $LASTEXITCODE if( $nunitExitCode -ne 0 ) { if (-not (Test-Path -Path $nunitReport -PathType Leaf)) { $msg = "NUnit didn't run successfully: NUnit returned exit code ""$($nunitExitCode)""." Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } else { $msg = "NUnit tests failed: NUnit returned exit code ""$($nunitExitCode)""." Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } } } function Invoke-WhiskeyParallelTask { [CmdletBinding()] [Whiskey.Task('Parallel')] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory)] [hashtable] $TaskParameter, [TimeSpan] $Timeout = (New-TimeSpan -Minutes 10) ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $queues = $TaskParameter['Queues'] if( -not $queues ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property "Queues" is mandatory. It should be an array of queues to run. Each queue should contain a "Tasks" property that is an array of task to run, e.g. Build: - Parallel: Queues: - Tasks: - TaskOne - TaskTwo - Tasks: - TaskOne ' return } try { $jobs = New-Object 'Collections.ArrayList' $queueIdx = -1 $numTimedOut = 0 foreach( $queue in $queues ) { $queueIdx++ $whiskeyModulePath = Join-Path -Path $whiskeyScriptRoot -ChildPath 'Whiskey.psd1' -Resolve if( -not $queue.ContainsKey('Tasks') ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Queue[{0}]: Property "Tasks" is mandatory. Each queue should have a "Tasks" property that is an array of Whiskey task to run, e.g. Build: - Parallel: Queues: - Tasks: - TaskOne - TaskTwo - Tasks: - TaskOne ' -f $queueIdx); return } Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}] Starting background queue.' -f $queueIdx) $serializableContext = $TaskContext | ConvertFrom-WhiskeyContext $taskPathsTasks = $queue['Tasks'] | ForEach-Object { $taskName,$taskParameter = ConvertTo-WhiskeyTask -InputObject $_ -ErrorAction Stop [pscustomobject]@{ Name = $taskName; Parameter = $taskParameter } } $taskModulePaths = Get-WhiskeyTask | ForEach-Object { Get-Command -Name $_.CommandName } | Select-Object -ExpandProperty 'Module' | Select-Object -ExpandProperty 'Path' | Select-Object -Unique if( $taskModulePaths ) { $msg = "Found $(($taskModulePaths | Measure-Object).Count) module(s) containing Whiskey tasks:" Write-WhiskeyDebug -Context $TaskContext -Message $msg $taskModulePaths | ForEach-Object { "* $($_)" } | Write-Debug } else { Write-WhiskeyDebug -Context $TaskContext -Message 'Found no loaded modules that contain Whiskey tasks.' } Write-WhiskeyInfo -Context $TaskContext -Message "Starting background job #$($queueIdx)." $job = Start-Job -ScriptBlock { Set-StrictMode -Version 'Latest' # Progress bars in background jobs seem to cause problems. $Global:ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue $VerbosePreference = $using:VerbosePreference $DebugPreference = $using:DebugPreference $InformationPreference = $using:InformationPreference $WarningPreference = $using:WarningPreference $ErrorActionPreference = $using:ErrorActionPreference $whiskeyModulePath = $using:whiskeyModulePath $serializedContext = $using:serializableContext & { Import-Module -Name $whiskeyModulePath } 4> $null [Whiskey.Context]$context = $serializedContext | ConvertTo-WhiskeyContext # Load third-party tasks. foreach( $info in $context.TaskPaths ) { Write-WhiskeyDebug -Context $context -Message ('Loading task from "{0}".' -f $info.FullName) . $info.FullName } # Load modules containing third-party tasks. foreach( $modulePath in $using:taskModulePaths ) { Write-WhiskeyDebug -Context $context -Message "Loading task module ""$($modulePath)""." Import-Module -Name $modulePath -Global } foreach( $task in $using:taskPathsTasks ) { Write-WhiskeyDebug -Context $context -Message ($task.Name) $task.Parameter | ConvertTo-Json -Depth 50 | Write-WhiskeyDebug -Context $context Invoke-WhiskeyTask -TaskContext $context -Name $task.Name -Parameter $task.Parameter } } $job | Add-Member -MemberType NoteProperty -Name 'QueueIndex' -Value $queueIdx [Void]$jobs.Add($job) } $taskDuration = [Diagnostics.Stopwatch]::StartNew() foreach( $job in $jobs ) { $msg = "Watching background job #$($job.QueueIndex) $($job.Name)." Write-WhiskeyInfo -Context $TaskContext -Message $msg Write-WhiskeyDebug -Context $TaskContext -Message "Job #$($job.QueueIndex) $($job.Name)" do { Write-WhiskeyDebug -Context $TaskContext -Message " Waiting for 9 seconds." $completedJob = $job | Wait-Job -Timeout 9 if( $job.HasMoreData ) { Write-WhiskeyDebug -Context $TaskContext -Message " Receiving output." # There's a bug where Write-Host output gets duplicated by Receive-Job if $InformationPreference is set to "Continue". # Since some things use Write-Host, this is a workaround to avoid seeing duplicate host output. $job | Receive-Job -InformationAction SilentlyContinue } if( $completedJob ) { $duration = $job.PSEndTime - $job.PSBeginTime $msg = "Background job #$($job.QueueIndex) $($job.Name) is ""$($job.State.ToString())"" in " + "$([int]$duration.TotalMinutes)m$($duration.Seconds)s." Write-WhiskeyInfo -Context $TaskContext -Message $msg if( $job.JobStateInfo.State -ne [Management.Automation.JobState]::Completed ) { $msg = "Background job #$($job.QueueIndex) $($job.Name) didn't finish successfully but ended " + "in state ""$($job.State.ToString())""." Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } break } if( $taskDuration.Elapsed -gt $Timeout ) { $duration = (Get-Date) - $job.PSBeginTime $msg = "Background job #$($job.QueueIndex) $($job.Name) is still running after " + "$([int]$duration.TotalMinutes)m$($duration.Seconds)s which is longer than the " + "$([int]$Timeout.TotalMinutes)m$($Timeout.Seconds)s timeout. It's final state is " + """$($job.State.ToString())""." Write-WhiskeyError -Context $TaskContext -Message $msg $numTimedOut += 1 break } } while( $true ) } if( $numTimedOut ) { $msg = "$($numTimedOut) background jobs timed out without completing in $([int]$Timeout.TotalMinutes)m" + "$($Timeout.Seconds)s." Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } } finally { $jobs | Stop-Job $jobs | Remove-Job } } function Invoke-WhiskeyPesterTask { [Whiskey.Task('Pester')] [Whiskey.RequiresPowerShellModule('Pester', Version='5.*', ModuleInfoParameterName='PesterModuleInfo', SkipImport)] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Management.Automation.PSModuleInfo] $PesterModuleInfo, [hashtable] $Configuration, [hashtable] $Container ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $pesterManifestPath = $PesterModuleInfo.Path $exitCodePath = Join-Path -Path $TaskContext.Temp -ChildPath 'exitcode' $cmdArgList = @{ WorkingDirectory = (Get-Location).Path; PesterManifestPath = $pesterManifestPath; Configuration = $Configuration; Container = $Container; ExitCodePath = $exitCodePath; Preference = @{ 'VerbosePreference' = [String]$VerbosePreference; 'DebugPreference' = [String]$DebugPreference; 'ProgressPreference' = [String]$ProgressPreference; 'WarningPreference' = [String]$WarningPreference; 'ErrorActionPreference' = [String]$ErrorActionPreference; 'InformationPreference' = [String]$InformationPreference; } } $cmdName = 'powershell' if ($PSVersionTable['PSEdition'] -eq 'Core') { $cmdName = 'pwsh' } $invokePesterPath = Join-Path -Path $script:whiskeyBinPath -ChildPath 'Invoke-Pester.ps1' -Resolve Write-WhiskeyDebug "Starting ${cmdName}" $parameterJson = $cmdArgList | ConvertTo-Json -Depth 100 $parameterBytes = [Text.Encoding]::Unicode.GetBytes($parameterJson) $parameterBase64 = [Convert]::ToBase64String($parameterBytes) & $cmdName -NoProfile -NonInteractive -File $invokePesterPath -ParameterBase64 $parameterBase64 Write-WhiskeyDebug "Done ${cmdName}" if( $Configuration.ContainsKey('TestResult') -and ` $Configuration['TestResult'] -is [Collections.ICollection] -and ` $Configuration['TestResult'].ContainsKey('OutputPath') ) { Publish-WhiskeyPesterTestResult -Path $Configuration['TestResult']['OutputPath'] } if (-not (Test-Path -Path $exitCodePath -PathType Leaf)) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "Pester task failed to run tests." return } [int] $exitCode = Get-Content -Path $exitCodePath -ReadCount 1 if( $exitCode -ne 0 ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "Tests failed with exit code $($exitCode)." } } function Invoke-WhiskeyPester3Task { [Whiskey.Task('Pester3',Platform='Windows', Obsolete, ObsoleteMessage='The "Pester3" task is obsolete and is no longer supported.')] [Whiskey.RequiresPowerShellModule('Pester', ModuleInfoParameterName='PesterModuleInfo', Version='3.*', SkipImport)] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(Mandatory)] [String[]]$Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $outputFile = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('pester+{0}.xml' -f [IO.Path]::GetRandomFileName()) $outputFile = [IO.Path]::GetFullPath($outputFile) $moduleInfo = $TaskParameter['PesterModuleInfo'] $pesterManifestPath = $moduleInfo.Path $workingDirectory = (Get-Location).Path # We do this in the background so we can test this with Pester. $job = Start-Job -ScriptBlock { $VerbosePreference = $using:VerbosePreference $DebugPreference = $using:DebugPreference $ProgressPreference = $using:ProgressPreference $WarningPreference = $using:WarningPreference $ErrorActionPreference = $using:ErrorActionPreference Set-Location -Path $using:workingDirectory $script = $using:Path $pesterManifestPath = $using:pesterManifestPath $outputFile = $using:outputFile Invoke-Command -ScriptBlock { $VerbosePreference = 'SilentlyContinue' Import-Module -Name $pesterManifestPath -WarningAction Ignore } Invoke-Pester -Script $script -OutputFile $outputFile -OutputFormat NUnitXml -PassThru } # There's a bug where Write-Host output gets duplicated by Receive-Job if $InformationPreference is set to "Continue". # Since Pester uses Write-Host, this is a workaround to avoid seeing duplicate Pester output. do { $job | Receive-Job -InformationAction SilentlyContinue } while( -not ($job | Wait-Job -Timeout 1) ) $job | Receive-Job -InformationAction SilentlyContinue Publish-WhiskeyPesterTestResult -Path $outputFile $result = [xml](Get-Content -Path $outputFile -Raw) if( -not $result ) { throw ('Unable to parse Pester output XML report ''{0}''.' -f $outputFile) } if( $result.'test-results'.errors -ne '0' -or $result.'test-results'.failures -ne '0' ) { throw ('Pester tests failed.') } } function Invoke-WhiskeyPester4Task { [Whiskey.Task('Pester4')] [Whiskey.RequiresPowerShellModule('Pester', ModuleInfoParameterName='PesterModuleInfo', Version='4.*', SkipImport)] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Alias('Path')] [object]$Script, [String[]]$Exclude, [int]$DescribeDurationReportCount = 0, [int]$ItDurationReportCount = 0, [Management.Automation.PSModuleInfo]$PesterModuleInfo, [Object]$Argument = @{}, [switch]$NoJob ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $Exclude ) { $Exclude = $Exclude | Convert-WhiskeyPathDirectorySeparator } if( -not $Script ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Script' -Message ('Script is mandatory.') return } $Script = & { foreach( $scriptItem in $Script ) { $path = $null if( $scriptItem -is [String] ) { $path = $scriptItem } elseif( $scriptItem | Get-Member -Name 'Keys' ) { $path = $scriptItem['Path'] $numPaths = ($path | Measure-Object).Count if( $numPaths -gt 1 ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Script' -Message ('when passing a hashtable to Pester''s "Script" parameter, the "Path" value must be a single string. We got {0} strings: {1}' -f $numPaths,($path -join ', ')) continue } } $path = Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Script' -Path $path -Mandatory foreach( $pathItem in $path ) { if( $Exclude ) { $excluded = $false foreach( $exclusion in $Exclude ) { if( $pathItem -like $exclusion ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('EXCLUDE {0} -like {1}' -f $pathItem,$exclusion) $excluded = $true } else { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0} -notlike {1}' -f $pathItem,$exclusion) } } if( $excluded ) { continue } } if( $scriptItem -is [String] ) { Write-Output $pathItem continue } if( $scriptItem | Get-Member -Name 'Keys' ) { $newScriptItem = $scriptItem.Clone() $newScriptItem['Path'] = $pathItem Write-Output $newScriptItem } } } } if( -not $Script ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Found no tests to run.') return } $pesterManifestPath = $PesterModuleInfo.Path $Argument['Script'] = $Script $Argument['PassThru'] = $true if( $Argument.ContainsKey('OutputFile') ) { $outputFile = $Argument['OutputFile'] } else { $outputFileRoot = Resolve-Path -Path $TaskContext.OutputDirectory -Relative $outputFile = Join-Path -Path $outputFileRoot -ChildPath ('pester+{0}.xml' -f [IO.Path]::GetRandomFileName()) $Argument['OutputFile'] = $outputFile } if( -not $Argument.ContainsKey('OutputFormat') ) { $Argument['OutputFormat'] = 'NUnitXml' } $Argument | Write-WhiskeyObject -Context $context -Level Verbose $args = @( (Get-Location).Path, $pesterManifestPath, $Argument, @{ 'VerbosePreference' = $VerbosePreference; 'DebugPreference' = $DebugPreference; 'ProgressPreference' = $ProgressPreference; 'WarningPreference' = $WarningPreference; 'ErrorActionPreference' = $ErrorActionPreference; } ) $cmdName = 'Start-Job' if( $NoJob ) { $cmdName = 'Invoke-Command' } $result = & $cmdName -ArgumentList $args -ScriptBlock { param( [String]$WorkingDirectory, [String]$PesterManifestPath, [hashtable]$Parameter, [hashtable]$Preference ) Set-Location -Path $WorkingDirectory $VerbosePreference = 'SilentlyContinue' Import-Module -Name $PesterManifestPath -Verbose:$false -WarningAction Ignore $VerbosePreference = $Preference['VerbosePreference'] $DebugPreference = $Preference['DebugPreference'] $ProgressPreference = $Preference['ProgressPreference'] $WarningPreference = $Preference['WarningPreference'] $ErrorActionPreference = $Preference['ErrorActionPreference'] Invoke-Pester @Parameter } if( -not $NoJob ) { $result = $result | Receive-Job -Wait -AutoRemoveJob -InformationAction Ignore } $result.TestResult | Group-Object 'Describe' | ForEach-Object { $totalTime = [TimeSpan]::Zero $_.Group | ForEach-Object { $totalTime += $_.Time } [pscustomobject]@{ Describe = $_.Name; Duration = $totalTime } } | Sort-Object -Property 'Duration' -Descending | Select-Object -First $DescribeDurationReportCount | Format-Table -AutoSize $result.TestResult | Sort-Object -Property 'Time' -Descending | Select-Object -First $ItDurationReportCount | Format-Table -AutoSize -Property 'Describe','Name','Time' Publish-WhiskeyPesterTestResult -Path $outputFile $outputFileContent = Get-Content -Path $outputFile -Raw $outputFileContent | Write-WhiskeyDebug $result = [xml]$outputFileContent if( -not $result ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to parse Pester output XML report "{0}".' -f $outputFile) return } if( $result.DocumentElement.errors -ne '0' -or $result.DocumentElement.failures -ne '0' ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Pester tests failed.') return } } function Invoke-WhiskeyPipelineTask { [CmdletBinding()] [Whiskey.Task('Pipeline',SupportsClean,SupportsInitialize)] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $TaskParameter['Name'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Name is a mandatory property, but is missing or doesn''t have a value. It should be set to a list of pipeline names you want to run as part of another pipeline, e.g. Build: - Pipeline: Name: - One - Two One: - TASK Two: - TASK ') return } $currentPipeline = $TaskContext.PipelineName try { foreach( $name in $TaskParameter['Name'] ) { Invoke-WhiskeyPipeline -Context $TaskContext -Name $name } } finally { $TaskContext.PipelineName = $currentPipeline } } function Invoke-WhiskeyPowerShell { [Whiskey.Task('PowerShell', SupportsClean, SupportsInitialize)] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Whiskey.Tasks.ValidatePath(PathType='File')] [String[]]$Path, [String]$ScriptBlock, [Object]$Argument = @() ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $scriptBlockGiven = $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('ScriptBlock') if( -not $Path -and -not $scriptBlockGiven ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Missing required property. Task must use one of "Path" or "ScriptBlock".' return } elseif( $Path -and $scriptBlockGiven ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Task uses both "Path" and "ScriptBlocK" properties. Only one of these properties is allowed.' return } if( $scriptBlockGiven ) { $Path = Join-Path -Path $TaskContext.Temp.FullName -ChildPath 'scriptblock.ps1' Set-Content -Path $Path -Value $ScriptBlock -Force } $workingDirectory = (Get-Location).ProviderPath foreach( $scriptPath in $Path ) { $mediumAndPath = "script `"$($scriptPath)`"" if( $scriptBlockGiven ) { $mediumAndPath = 'script block' } $scriptCommand = Get-Command -Name $scriptPath -ErrorAction Ignore if( -not $scriptCommand ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "Can't run PowerShell $($mediumAndPath): it has a syntax error." continue } $passTaskContext = $scriptCommand.Parameters.ContainsKey('TaskContext') if( (Get-Member -InputObject $argument -Name 'Keys') ) { $scriptCommand.Parameters.Values | Where-Object { $_.ParameterType -eq [switch] } | Where-Object { $argument.ContainsKey($_.Name) } | ForEach-Object { $argument[$_.Name] = $argument[$_.Name] | ConvertFrom-WhiskeyYamlScalar } } $resultPath = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('PowerShell-{0}-RunResult-{1}' -f ($scriptPath | Split-Path -Leaf),([IO.Path]::GetRandomFileName())) $serializableContext = $TaskContext | ConvertFrom-WhiskeyContext $job = Start-Job -ScriptBlock { Set-StrictMode -Version 'Latest' $VerbosePreference = $using:VerbosePreference $DebugPreference = $using:DebugPreference $ProgressPreference = $using:ProgressPreference $WarningPreference = $using:WarningPreference $ErrorActionPreference = $using:ErrorActionPreference $InformationPreference = $using:InformationPreference $workingDirectory = $using:WorkingDirectory $scriptPath = $using:ScriptPath $argument = $using:argument $serializedContext = $using:serializableContext $whiskeyScriptRoot = $using:whiskeyScriptRoot $resultPath = $using:resultPath $passTaskContext = $using:passTaskContext $scriptBlockGiven = $using:scriptBlockGiven Invoke-Command -ScriptBlock { $VerbosePreference = 'SilentlyContinue'; & (Join-Path -Path $whiskeyScriptRoot -ChildPath 'Import-Whiskey.ps1' -Resolve -ErrorAction Stop) } [Whiskey.Context]$context = $serializedContext | ConvertTo-WhiskeyContext Set-Location $workingDirectory $scriptPath = Resolve-Path -Path $scriptPath -Relative if( $scriptBlockGiven ) { $message = '' $lines = Get-Content -Path $scriptPath if( ($lines | Measure-Object).Count -le 1 ) { Write-WhiskeyInfo -Context $context -Message ($lines | Select-Object -First 1) } else { & { '' | Write-Output $lines | Write-Output '' | Write-Output } | Write-WhiskeyInfo -NoTiming } } else { $message = $scriptPath if( $message.Contains(' ') ) { $message = '& "{0}"' -f $message } } $contextArgument = @{ } if( $passTaskContext ) { $contextArgument['TaskContext'] = $context if( $message ) { $message = '{0} -TaskContext $context' -f $message } } if( $argument ) { $argumentDesc = & { if( ($argument | Get-Member -Name 'Keys') ) { foreach( $parameterName in $argument.Keys ) { Write-Output ('-{0}' -f $parameterName) Write-Output $argument[$parameterName] } } else { Write-Output $argument } } | ForEach-Object { if( $_.ToString().Contains(' ') ) { Write-Output ("{0}" -f $_) return } Write-Output $_ } if( $message ) { $message = '{0} {1}' -f $message,($argumentDesc -join ' ') } } if( $message ) { Write-WhiskeyInfo -Context $context -Message $message } $Global:LASTEXITCODE = 0 $result = [pscustomobject]@{ 'ExitCode' = $Global:LASTEXITCODE 'Successful' = $false } $result | ConvertTo-Json | Set-Content -Path $resultPath try { Set-StrictMode -Off & $scriptPath @contextArgument @argument $result.ExitCode = $Global:LASTEXITCODE $result.Successful = $? } catch { $_ | Out-String | Write-WhiskeyError } Set-StrictMode -Version 'Latest' Write-WhiskeyVerbose -Context $context -Message ('Exit Code {0}' -f $result.ExitCode) Write-WhiskeyVerbose -Context $context -Message ('$? {0}' -f $result.Successful) $result | ConvertTo-Json | Set-Content -Path $resultPath } do { # There's a bug where Write-Host output gets duplicated by Receive-Job if $InformationPreference is set to "Continue". # Since some things use Write-Host, this is a workaround to avoid seeing duplicate host output. $job | Receive-Job -InformationAction SilentlyContinue } while( -not ($job | Wait-Job -Timeout 1) ) $job | Receive-Job -InformationAction SilentlyContinue if( (Test-Path -Path $resultPath -PathType Leaf) ) { $runResult = Get-Content -Path $resultPath -Raw | ConvertFrom-Json } else { Stop-WhiskeyTask -TaskContext $TaskContext -Message "PowerShell $($mediumAndPath) threw a terminating exception." return } if( $runResult.ExitCode -ne 0 ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "PowerShell $($mediumAndPath) failed, exited with code $($runResult.ExitCode)." return } elseif( -not $runResult.Successful ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "PowerShell $($mediumAndPath) threw a terminating exception." return } } } function New-WhiskeyProGetUniversalPackage { [CmdletBinding()] [Whiskey.Task('ProGetUniversalPackage')] [Whiskey.RequiresPowerShellModule('ProGetAutomation', Version='3.*', VersionParameterName='ProGetAutomationVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(PathType='Directory')] [String]$SourceRoot, [String[]] $Include, [String[]] $Exclude ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $manifestProperties = @{} if( $TaskParameter.ContainsKey('ManifestProperties') ) { $manifestProperties = $TaskParameter['ManifestProperties'] foreach( $taskProperty in @( 'Name', 'Description', 'Version' )) { if( $manifestProperties.Keys -contains $taskProperty ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('"ManifestProperties" contains key "{0}". This property cannot be manually defined in "ManifestProperties" as it is set automatically from the corresponding task property "{0}".' -f $taskProperty) return } } } foreach( $mandatoryProperty in @( 'Name', 'Description' ) ) { if( -not $TaskParameter.ContainsKey($mandatoryProperty) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "{0}" is mandatory.' -f $mandatoryProperty) return } } $name = $TaskParameter['Name'] $validNameRegex = '^[0-9A-z\-\._]{1,50}$' if ($name -notmatch $validNameRegex) { Stop-WhiskeyTask -TaskContext $TaskContext -Message '"Name" property is invalid. It should be a string of one to fifty characters: numbers (0-9), upper and lower-case letters (A-z), dashes (-), periods (.), and underscores (_).' return } $version = $TaskParameter['Version'] if( $version ) { [SemVersion.SemanticVersion]$semVer = $null if( -not ([SemVersion.SemanticVersion]::TryParse($version, [ref]$semVer)) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Version" is not a valid semantic version.') return } # If someone has provided their own version, use it. $version = New-WhiskeyVersionObject -SemVer $semVer $packageVersion = $semVer.ToString() } else { $version = $TaskContext.Version # ProGet uses build metadata to distinguish different versions (i.e. 2.0.1+build.1 is different than # 2.0.1+build.2), which means users could inadvertently release multiple versions of a package. Remove the # build metadata to prevent this. This should be what people expect most of the time. $packageVersion = $version.SemVer2NoBuildMetadata.ToString() } $compressionLevel = [IO.Compression.CompressionLevel]::Optimal if( $TaskParameter['CompressionLevel'] ) { $expectedValues = [Enum]::GetValues([IO.Compression.CompressionLevel]) $compressionLevel = $TaskParameter['CompressionLevel'] if( $compressionLevel -notin $expectedValues ) { [int]$intCompressionLevel = 0 if( -not [int]::TryParse($TaskParameter['CompressionLevel'],[ref]$intCompressionLevel) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "CompressionLevel": "{0}" is not a valid compression level. It must be one of: {1}' -f $TaskParameter['CompressionLevel'],($expectedValues -join ', ')); return } $compressionLevel = $intCompressionLevel if( $compressionLevel -ge 5 ) { $compressionLevel = [IO.Compression.CompressionLevel]::Optimal } else { $compressionLevel = [IO.Compression.CompressionLevel]::Fastest } Write-WhiskeyWarning -Context $TaskContext -Message ('The ProGetUniversalPackage task no longer supports integer-style compression levels. Please update your task in your whiskey.yml file to use one of the new values: {0}. We''re converting the number you provided, "{1}", to "{2}".' -f ($expectedValues -join ', '),$TaskParameter['CompressionLevel'],$compressionLevel) } } $Include = $Include | Where-Object { $_ } if ($null -eq $Include) { $Include = @() } $Exclude = $Exclude | Where-Object { $_ } if ($null -eq $Exclude) { $Exclude = @() } function Write-PackagedItemInfo { [CmdletBinding(DefaultParameterSetName='Path')] param( [Parameter(Mandatory)] [String] $Path, [switch] $Unfiltered, [String] $DestinationPath, [Parameter(Mandatory, ParameterSetName='Included')] [switch] $Included, [Parameter(Mandatory, ParameterSetName='Excluded')] [switch] $Excluded ) $flag = '' if ($Included -or $Excluded) { $flag = ' + ' if ($Excluded) { $flag = ' - ' } } if (Test-Path -Path $Path -PathType Container) { $childPath = '\' if ($Unfiltered) { $childPath = '\**' } $Path = Join-Path -Path $Path -ChildPath $childPath if ($Path -eq '.\') { $Path = '.' } } $destinationMsg = '' if ($DestinationPath) { $destinationMsg = " → ${DestinationPath}" } $msg = " ${flag}${Path}${destinationMsg}" Write-WhiskeyInfo -Context $TaskContext -Message $msg } function Copy-ToPackage { param( [Parameter(Mandatory)] [Object[]] $Path, [switch] $AsThirdPartyItem ) foreach ($item in $Path) { $override = $false if (Get-Member -InputObject $item -Name 'Keys') { $sourcePath = $null $override = $true foreach( $key in $item.Keys ) { $destinationItemName = $item[$key] $sourcePath = $key } } else { $sourcePath = $item } $pathparam = 'Path' if( $AsThirdPartyItem ) { $pathparam = 'ThirdPartyPath' } $sourcePaths = $sourcePath | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName $pathparam if( -not $sourcePaths ) { return } $basePath = (Get-Location).Path foreach( $sourcePath in $sourcePaths ) { $addParams = @{ BasePath = $basePath } $destPathMsgArg = @{} if( $override ) { $addParams = @{ PackageItemName = $destinationItemName } $destPathMsgArg['DestinationPath'] = $destinationItemName } $addParams['CompressionLevel'] = $compressionLevel if( $AsThirdPartyItem ) { Write-PackagedItemInfo -Path $sourcePath @destPathMsgArg -Unfiltered Get-Item -Path $sourcePath | Add-ProGetUniversalPackageFile -PackagePath $outFile @addParams -ErrorAction Stop continue } if( (Test-Path -Path $sourcePath -PathType Leaf) ) { Write-PackagedItemInfo -Path $sourcePath @destPathMsgArg Add-ProGetUniversalPackageFile -PackagePath $outFile -InputObject $sourcePath @addParams -ErrorAction Stop continue } if (-not $Include) { $msg = "Property ""Include"" is mandatory because ""${sourcePath}"" is in your ""Path"" property " + 'and it is a directory. The "Include" property is a whitelist of files (wildcards ' + 'supported) to include in your package. Only files in directories that match an item in ' + 'the "Include" list will be added to your package.' Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } # include/exclude items that contain directory seperators should be matched against an item's path $nameIncPatterns = $Include | Where-Object { -not ($_ | Split-Path) } $pathIncPatterns = $Include | Where-Object { $_ | Split-Path } | ForEach-Object { [wildcardpattern]::Unescape($_) } | ForEach-Object { [wildcardpattern]::Escape($_) } | Resolve-WhiskeyRelativePath | ForEach-Object { [wildcardpattern]::Unescape($_) } $nameExcPatterns = $Exclude | Where-Object { -not ($_ | Split-Path) } $pathExcPatterns = $Exclude | Where-Object { $_ | Split-Path } | ForEach-Object { [wildcardpattern]::Unescape($_) } | ForEach-Object { [wildcardpattern]::Escape($_) } | Resolve-WhiskeyRelativePath | ForEach-Object { [wildcardpattern]::Unescape($_) } function Find-Item { param( [Parameter(Mandatory)] $Path ) if( (Test-Path -Path $Path -PathType Leaf) ) { return Get-Item -Path $Path } $Path = Join-Path -Path $Path -ChildPath '*' & { Get-ChildItem -Path $Path -Include $nameIncPatterns -Exclude $nameExcPatterns -File Get-Item -Path $Path -Exclude $nameExcPatterns | Where-Object { $_.PSIsContainer } } | ForEach-Object { if ($pathIncPatterns -or $pathExcPatterns) { # Resolve path-based include and exclude patterns using relative paths. $_ | Add-Member -Name 'RelativePath' ` -MemberType NoteProperty ` -Value ($_ | Resolve-WhiskeyRelativePath) } return $_ } | Where-Object { if (-not $pathIncPatterns) { return $true } foreach ($pattern in $pathIncPatterns) { if ($_.RelativePath -like $pattern) { return $true } } Write-PackagedItemInfo -Path $_.RelativePath -Excluded return $false } | Where-Object { if (-not $pathExcPatterns) { return $true } foreach ($pattern in $pathExcPatterns) { if ($_.RelativePath -like $pattern) { Write-PackagedItemInfo -Path $_.RelativePath -Excluded return $false } } return $true } | ForEach-Object { if( $_.PSIsContainer ) { Find-Item -Path $_.FullName } else { $_ } } } if( $override ) { $overrideBasePath = Resolve-Path -Path $sourcePath | Select-Object -ExpandProperty 'ProviderPath' if( (Test-Path -Path $overrideBasePath -PathType Leaf) ) { $overrideBasePath = Split-Path -Parent -Path $overrideBasePath } $addParams['BasePath'] = $overrideBasePath $addParams.Remove('PackageItemName') $overrideInfo = ' -> {0}' -f $destinationItemName if ($destinationItemName -ne '.') { $addParams['PackageParentPath'] = $destinationItemName } } Write-PackagedItemInfo -Path $sourcePath @destPathMsgArg Find-Item -Path $sourcePath | Add-ProGetUniversalPackageFile -PackagePath $outFile @addParams -ErrorAction Stop } } } $tempRoot = Join-Path -Path $TaskContext.Temp -ChildPath 'upack' New-Item -Path $tempRoot -ItemType 'Directory' | Out-Null $tempPackageRoot = Join-Path -Path $tempRoot -ChildPath 'package' New-Item -Path $tempPackageRoot -ItemType 'Directory' | Out-Null $upackJsonPath = Join-Path -Path $tempRoot -ChildPath 'upack.json' $manifestProperties | ConvertTo-Json | Set-Content -Path $upackJsonPath # Add the version.json file $versionJsonPath = Join-Path -Path $tempPackageRoot -ChildPath 'version.json' @{ Version = $version.Version.ToString(); SemVer2 = $version.SemVer2.ToString(); SemVer2NoBuildMetadata = $version.SemVer2NoBuildMetadata.ToString(); PrereleaseMetadata = $version.SemVer2.Prerelease; BuildMetadata = $version.SemVer2.Build; SemVer1 = $version.SemVer1.ToString(); } | ConvertTo-Json -Depth 1 | Set-Content -Path $versionJsonPath $badChars = [IO.Path]::GetInvalidFileNameChars() | ForEach-Object { [regex]::Escape($_) } $fixRegex = '[{0}]' -f ($badChars -join '') $fileName = '{0}.{1}.upack' -f $name,($version.SemVer2NoBuildMetadata -replace $fixRegex,'-') $outFile = Join-Path -Path $TaskContext.OutputDirectory -ChildPath $fileName if( (Test-Path -Path $outFile -PathType Leaf) ) { Remove-Item -Path $outFile -Force } if( -not $manifestProperties.ContainsKey('title') ) { $manifestProperties['title'] = $TaskParameter['Name'] } $outFileDisplay = $outFile -replace ('^{0}' -f [regex]::Escape($TaskContext.BuildRoot)),'' $outFileDisplay = $outFileDisplay.Trim([IO.Path]::DirectorySeparatorChar) Write-WhiskeyInfo -Context $TaskContext -Message ('Creating universal package "{0}".' -f $outFileDisplay) New-ProGetUniversalPackage -OutFile $outFile ` -Version $packageVersion ` -Name $TaskParameter['Name'] ` -Description $TaskParameter['Description'] ` -AdditionalMetadata $manifestProperties Add-ProGetUniversalPackageFile -PackagePath $outFile -InputObject $versionJsonPath -ErrorAction Stop if( $SourceRoot ) { Write-WhiskeyWarning -Context $TaskContext -Message ('The "SourceRoot" property is obsolete. Please use the "WorkingDirectory" property instead.') Push-Location -Path $SourceRoot } try { if( $TaskParameter['Path'] ) { Copy-ToPackage -Path $TaskParameter['Path'] } if( $TaskParameter.ContainsKey('ThirdPartyPath') -and $TaskParameter['ThirdPartyPath'] ) { Copy-ToPackage -Path $TaskParameter['ThirdPartyPath'] -AsThirdPartyItem } } finally { if( $SourceRoot ) { Pop-Location } } } function Publish-WhiskeyBBServerTag { [CmdletBinding()] [Whiskey.Task('PublishBitbucketServerTag')] [Whiskey.RequiresPowerShellModule('BitbucketServerAutomation', Version='0.9.*', VersionParameterName='BitbucketServerAutomationVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $exampleTask = 'Publish: - PublishBitbucketServerTag: CredentialID: BitbucketServerCredential Uri: https://bitbucketserver.example.com' if( $TaskContext.BuildMetadata.IsPullRequest ) { 'Skipping PublishBitbucketServerTag task: can''t tag a pull request commit because it doesn''t exist in the ' + 'origin repostory, only on the build server.' | Write-WhiskeyVerbose return } if( -not $TaskParameter['CredentialID'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "Property 'CredentialID' is mandatory. It should be the ID of the credential to use when connecting to Bitbucket Server: $exampleTask Use the `Add-WhiskeyCredential` function to add credentials to the build. " return } if( -not $TaskParameter['Uri'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "Property 'Uri' is mandatory. It should be the URL to the instance of Bitbucket Server where the tag should be created: $exampleTask " return } $commitHash = $TaskContext.BuildMetadata.ScmCommitID if( -not $commitHash ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyDescription '' -Message ('Unable to identify a valid commit to tag. Are you sure you''re running under a build server?') return } if( $TaskParameter['ProjectKey'] -and $TaskParameter['RepositoryKey'] ) { $projectKey = $TaskParameter['ProjectKey'] $repoKey = $TaskParameter['RepositoryKey'] } elseif( $TaskContext.BuildMetadata.ScmUri -and $TaskContext.BuildMetadata.ScmUri.Segments ) { $uri = [Uri]$TaskContext.BuildMetadata.ScmUri $projectKey = $uri.Segments[-2].Trim('/') $repoKey = $uri.Segments[-1] -replace '\.git$','' } else { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyDescription '' -Message ("Unable to determine the repository where we should create the tag. Either create a `GIT_URL` environment variable that is the URI used to clone your repository, or add your repository''s project and repository keys as `ProjectKey` and `RepositoryKey` properties, respectively, on this task: Publish: - PublishBitbucketServerTag: CredentialID: $($TaskParameter['CredentialID']) Uri: $($TaskParameter['Uri']) ProjectKey: PROJECT_KEY RepositoryKey: REPOSITORY_KEY ") return } $credentialID = $TaskParameter['CredentialID'] $credential = Get-WhiskeyCredential -Context $TaskContext -ID $credentialID -PropertyName 'CredentialID' $connection = New-BBServerConnection -Credential $credential -Uri $TaskParameter['Uri'] $tag = $TaskContext.Version.SemVer2NoBuildMetadata $msg = "Tagging commit ""$($commitHash)"" with ""$($tag)"" in Bitbucket Server ""$($projectKey)"" project's " + """$($repoKey)"" repository at $($TaskParameter['Uri'])." Write-WhiskeyInfo $msg New-BBServerTag -Connection $connection ` -ProjectKey $projectKey ` -Force ` -RepositoryKey $repoKey ` -Name $tag ` -CommitID $commitHash ` -ErrorAction Stop } function Publish-WhiskeyBuildMasterPackage { [CmdletBinding()] [Whiskey.Task('PublishBuildMasterPackage')] [Whiskey.RequiresPowerShellModule('BuildMasterAutomation',Version='0.6.*',VersionParameterName='BuildMasterAutomationVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $applicationName = $TaskParameter['ApplicationName'] if( -not $applicationName ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''ApplicationName'' is mandatory. It must be set to the name of the application in BuildMaster where the package should be published.') return } $releaseName = $TaskParameter['ReleaseName'] if( -not $releaseName ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''ReleaseName'' is mandatory. It must be set to the release name in the BuildMaster application where the package should be published.') return } $buildmasterUri = $TaskParameter['Uri'] if( -not $buildmasterUri ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''Uri'' is mandatory. It must be set to the BuildMaster URI where the package should be published.') return } $apiKeyID = $TaskParameter['ApiKeyID'] if( -not $apiKeyID ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''ApiKeyID'' is mandatory. It should be the ID of the API key to use when publishing the package to BuildMaster. Use the `Add-WhiskeyApiKey` to add your API key.') return } $apiKey = Get-WhiskeyApiKey -Context $TaskContext -ID $TaskParameter['ApiKeyID'] -PropertyName 'ApiKeyID' $buildMasterSession = New-BMSession -Uri $TaskParameter['Uri'] -ApiKey $apiKey $version = $TaskContext.Version.SemVer2 $variables = $TaskParameter['PackageVariable'] $release = Get-BMRelease -Session $buildMasterSession -Application $applicationName -Name $releaseName -ErrorAction Stop if( -not $release ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to create and deploy a release package in BuildMaster. Either the ''{0}'' application doesn''t exist or it doesn''t have a ''{1}'' release.' -f $applicationName,$releaseName) return } $release | Format-List | Out-String | Write-WhiskeyVerbose -Context $TaskContext if( $TaskParameter['PackageName'] ) { $packageName = $TaskParameter['PackageName'] } else { $packageName = '{0}.{1}.{2}' -f $version.Major,$version.Minor,$version.Patch } $package = New-BMPackage -Session $buildMasterSession -Release $release -PackageNumber $packageName -Variable $variables -ErrorAction Stop $package | Format-List | Out-String | Write-WhiskeyVerbose -Context $TaskContext if( ConvertFrom-WhiskeyYamlScalar -InputObject $TaskParameter['SkipDeploy'] ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Skipping deploy. SkipDeploy property is true') } else { $optionalParams = @{ 'Stage' = $TaskParameter['StartAtStage'] } $deployment = Publish-BMReleasePackage -Session $buildMasterSession -Package $package @optionalParams -ErrorAction Stop $deployment | Format-List | Out-String | Write-WhiskeyVerbose -Context $TaskContext } } function Publish-WhiskeyNodeModule { [Whiskey.Task('PublishNodeModule')] [Whiskey.RequiresTool('Node',PathParameterName='NodePath',VersionParameterName='NodeVersion')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [String]$CredentialID, [String]$EmailAddress, [Uri]$NpmRegistryUri, [String]$Tag ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if (-not $NpmRegistryUri) { Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property ''NpmRegistryUri'' is mandatory and must be a URI. It should be the URI to the registry where the module should be published. E.g., Build: - PublishNodeModule: NpmRegistryUri: https://registry.npmjs.org/ ' return } if( -not $CredentialID ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''CredentialID'' is mandatory. It should be the ID of the credential to use when publishing to ''{0}'', e.g. Build: - PublishNodeModule: NpmRegistryUri: {0} CredentialID: NpmCredential Use the `Add-WhiskeyCredential` function to add the credential to the build. ' -f $NpmRegistryUri) return } $credential = Get-WhiskeyCredential -Context $TaskContext -ID $CredentialID -PropertyName 'CredentialID' $npmUserName = $credential.UserName if( -not $EmailAddress ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''EmailAddress'' is mandatory. It should be the e-mail address of the user publishing the module, e.g. Build: - PublishNodeModule: NpmRegistryUri: {0} CredentialID: {1} EmailAddress: somebody@example.com ' -f $NpmRegistryUri,$CredentialID) return } $npmConfigPrefix = '//{0}{1}:' -f $NpmRegistryUri.Authority,$NpmRegistryUri.LocalPath $npmCredPassword = $credential.GetNetworkCredential().Password $npmBytesPassword = [System.Text.Encoding]::UTF8.GetBytes($npmCredPassword) $npmPassword = [System.Convert]::ToBase64String($npmBytesPassword) $originalPackageJsonPath = Resolve-Path -Path 'package.json' | Select-Object -ExpandProperty 'ProviderPath' $backupPackageJsonPath = Join-Path -Path $TaskContext.Temp -ChildPath 'package.json' try { $packageNpmrc = New-Item -Path '.npmrc' -ItemType File -Force Add-Content -Path $packageNpmrc -Value ('{0}_password="{1}"' -f $npmConfigPrefix, $npmPassword) Add-Content -Path $packageNpmrc -Value ('{0}username={1}' -f $npmConfigPrefix, $npmUserName) Add-Content -Path $packageNpmrc -Value ('{0}email={1}' -f $npmConfigPrefix, $EmailAddress) Add-Content -Path $packageNpmrc -Value ('registry={0}' -f $NpmRegistryUri) Write-WhiskeyVerbose -Context $TaskContext -Message ('Creating .npmrc at {0}.' -f $packageNpmrc) Get-Content -Path $packageNpmrc | ForEach-Object { if( $_ -match '_password' ) { return $_ -replace '=(.*)$','=********' } return $_ } | Write-WhiskeyVerbose -Context $TaskContext Copy-Item -Path $originalPackageJsonPath -Destination $backupPackageJsonPath Invoke-WhiskeyNpmCommand -Name 'version' ` -ArgumentList $TaskContext.Version.SemVer2NoBuildMetadata, '--no-git-tag-version', '--allow-same-version' ` -BuildRootPath $TaskContext.BuildRoot ` -ErrorAction Stop Invoke-WhiskeyNpmCommand -Name 'prune' -ArgumentList '--production' -BuildRootPath $TaskContext.BuildRoot -ErrorAction Stop $publishArgumentList = @( if( $Tag ) { '--tag' $Tag } elseif( $TaskContext.Version.SemVer2.Prerelease ) { '--tag' Resolve-WhiskeyVariable -Context $TaskContext -Name 'WHISKEY_SEMVER2_PRERELEASE_ID' } ) Invoke-WhiskeyNpmCommand -Name 'publish' -ArgumentList $publishArgumentList -BuildRootPath $TaskContext.BuildRoot -ErrorAction Stop } finally { if (Test-Path -Path $packageNpmrc -PathType Leaf) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Removing .npmrc at {0}.' -f $packageNpmrc) Remove-Item -Path $packageNpmrc } if (Test-Path -Path $backupPackageJsonPath -PathType Leaf) { Copy-Item -Path $backupPackageJsonPath -Destination $originalPackageJsonPath -Force } } } function Publish-WhiskeyPowerShellModule { [Whiskey.Task('PublishPowerShellModule')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='Directory')] [String] $Path, [Whiskey.Tasks.ValidatePath(PathType='File')] [String] $ModuleManifestPath, [String] $RepositoryName, [Alias('RepositoryUri')] [String] $RepositoryLocation, [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] [String] $CredentialID, [String] $ApiKeyID ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $manifestPath = '{0}\{1}.psd1' -f $Path,($Path | Split-Path -Leaf) if( $ModuleManifestPath ) { $manifestPath = $ModuleManifestPath } if( -not (Test-Path -Path $manifestPath -PathType Leaf) ) { $msg = "Module manifest path ""$($manifestPath)"" either does not exist or is a directory." Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $manifest = Test-ModuleManifest -Path $manifestPath -ErrorAction Ignore -WarningAction Ignore if( $TaskContext.Version.SemVer2.Prerelease -and ` (-not ($manifest.PrivateData) -or ` -not ($manifest.PrivateData | Get-Member 'Keys') -or ` -not $manifest.PrivateData.ContainsKey('PSData') -or ` -not ($manifest.PrivateData['PSData'] | Get-Member 'Keys') -or ` -not $manifest.PrivateData['PSData'].ContainsKey('Prerelease')) ) { $msg = "Module manifest ""$($manifest.Path)"" is missing a ""Prerelease"" property. Please make sure the " + "manifest's PrivateData hashtable contains a PSData key with a Prerelease property, e.g. @{ PrivateData = @{ PSData = @{ Prerelease = ''; } } } " Stop-WhiskeyTask -TaskContext $Context -Message $msg return } $manifestContent = Get-Content $manifest.Path $versionString = 'ModuleVersion = ''{0}.{1}.{2}''' -f ( $TaskContext.Version.SemVer2.Major, $TaskContext.Version.SemVer2.Minor, $TaskContext.Version.SemVer2.Patch ) $manifestContent = $manifestContent -replace 'ModuleVersion\s*=\s*(''|")[^''"]*(''|")', $versionString $prereleaseString = 'Prerelease = ''{0}''' -f $TaskContext.Version.SemVer2.Prerelease $manifestContent = $manifestContent -replace 'Prerelease\s*=\s*(''|")[^''"]*(''|")', $prereleaseString $manifestContent | Set-Content $manifest.Path Publish-WhiskeyPSObject -Context $TaskContext -ModuleInfo $manifest -RepositoryName $RepositoryName ` -RepositoryLocation $RepositoryLocation -CredentialID $CredentialID -ApiKeyId $ApiKeyID } function Publish-WhiskeyPowerShellScript { [Whiskey.Task('PublishPowerShellScript')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')] [String] $Path, [String] $RepositoryName, [Alias('RepositoryUri')] [String] $RepositoryLocation, [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] [String] $CredentialID, [String] $ApiKeyID ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not (Test-Path -Path $Path -PathType Leaf) ) { $msg = "Script manifest path ""$($Path)"" either does not exist or is a directory." Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } try { $scriptManifest = Test-ScriptFileInfo -Path $Path } catch { Stop-WhiskeyTask -TaskContext $TaskContext -Message $_ return } $manifestContent = Get-Content $scriptManifest.Path $versionString = ".VERSION $($TaskContext.Version.SemVer2NoBuildMetadata)" $manifestContent = $manifestContent -replace '.VERSION\s[^''"]*', $versionString $manifestContent | Set-Content $scriptManifest.Path Publish-WhiskeyPSObject -Context $TaskContext -ScriptInfo $scriptManifest -RepositoryName $RepositoryName ` -RepositoryLocation $RepositoryLocation -CredentialID $CredentialID -ApiKeyId $ApiKeyID } function Publish-WhiskeyProGetAsset { [Whiskey.Task('PublishProGetAsset')] [Whiskey.RequiresPowerShellModule('ProGetAutomation', Version='3.*', VersionParameterName='ProGetAutomationVersion')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] [CmdletBinding()] param( # The context this task is operating in. Use `New-WhiskeyContext` to create context objects. [Whiskey.Context]$TaskContext, [String[]] $Path, [String[]] $AssetPath, [String] $AssetDirectory, [String] $CredentialID, [Alias('Uri')] [Uri] $Url, [String] $ContentType ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $documentationMsg = "See the PublishProGetAsset task documentation for details: https://github.com/webmd-health-services/Whiskey/wiki/PublishProGetAsset-Task" if (-not $Path) { $msg = """Path"" is a mandatory property. It must be a list of relative paths to the files/directories to " + "upload to ProGet. Paths are relative to the whiskey.yml file. ${documentationMsg}" Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } if (-not $AssetDirectory) { $msg = """AssetDirectory"" is a mandatory property. It must be the root asset directory in ProGet where the item " + "will be uploaded to. ${documentationMsg}" Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } if (-not $CredentialID) { $msg = """CredentialID"" is a mandatory property. It should be the ID of the Whiskey credential to use when " + "connecting to ProGet. Add the credential to your build with the `Add-WhiskeyCredential` function. ${documentationMsg}" Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $credential = Get-WhiskeyCredential -Context $TaskContext -ID $CredentialID -PropertyName 'CredentialID' $session = New-ProGetSession -Uri $Url -Credential $credential -WarningAction Ignore $optionalArgs = @{} if ($ContentType) { $optionalArgs['ContentType'] = $ContentType } $assetDirName = $AssetDirectory Write-WhiskeyInfo $Url foreach($pathItem in $Path) { if ($AssetPath -and (($AssetPath | Measure-Object).Count -eq ($Path | Measure-Object).Count)) { $name = @($AssetPath)[$Path.indexOf($pathItem)] } else { $msg = "There must be the same number of ""Path"" items as ""AssetPath"" items. For each asset ""Path"" " + "there must be a respective ""AssetPath"" item which will be the item's path within the ProGet " + "asset directory. ${documentationMsg}" Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } Write-WhiskeyInfo " $($pathItem | Resolve-WhiskeyRelativePath) -> ${assetDirName}/${name}" Set-ProGetAsset -Session $session -DirectoryName $assetDirName -Path $name -FilePath $pathItem @optionalArgs } } function Publish-WhiskeyProGetUniversalPackage { [CmdletBinding()] [Whiskey.Task('PublishProGetUniversalPackage')] [Whiskey.RequiresPowerShellModule('ProGetAutomation', Version='3.*', VersionParameterName='ProGetAutomationVersion')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(AllowNonexistent, PathType='File')] [String[]]$Path, [Alias('Uri')] [Uri] $Url ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $exampleTask = 'Publish: - PublishProGetUniversalPackage: CredentialID: ProGetCredential Url: https://proget.example.com FeedName: UniversalPackages' if( -not $TaskParameter['CredentialID'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "CredentialID is a mandatory property. It should be the ID of the credential to use when connecting to ProGet: $exampleTask Use the `Add-WhiskeyCredential` function to add credentials to the build." return } if (-not $Url) { $msg = 'Url is a mandatory property. It should be the URL to the ProGet instance where you want to publish ' + "your package: $exampleTask " Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } if( -not $TaskParameter['FeedName'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "FeedName is a mandatory property. It should be the name of the universal feed in ProGet where you want to publish your package: $exampleTask " return } $credential = Get-WhiskeyCredential -Context $TaskContext -ID $TaskParameter['CredentialID'] -PropertyName 'CredentialID' $session = New-ProGetSession -Uri $Url -Credential $credential -WarningAction Ignore if( -not $Path ) { $Path = Join-Path -Path $TaskContext.OutputDirectory -ChildPath '*.upack' | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -AllowNonexistent -PropertyName 'Path' -PathType 'File' } $allowMissingPackages = $false if( $TaskParameter.ContainsKey('AllowMissingPackage') ) { $allowMissingPackages = $TaskParameter['AllowMissingPackage'] | ConvertFrom-WhiskeyYamlScalar } $packages = $Path | Where-Object { if( -not $TaskParameter.ContainsKey('Exclude') ) { return $true } foreach( $exclusion in $TaskParameter['Exclude'] ) { if( $_ -like $exclusion ) { return $false } } return $true } if( $allowMissingPackages -and -not $packages ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('There are no packages to publish.') return } if( -not $packages ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyDescription '' -Message ('Found no packages to publish. By default, the PublishProGetUniversalPackage task publishes all files with a .upack extension in the output directory. Check your whiskey.yml file to make sure you''re running the `ProGetUniversalPackage` task before this task (or some other task that creates universal ProGet packages). To publish other .upack files, set this task''s `Path` property to the path to those files. If you don''t want your build to fail when there are missing packages, then set this task''s `AllowMissingPackage` property to `true`.' -f $TaskContext.OutputDirectory) return } $feedName = $TaskParameter['FeedName'] $optionalParam = @{ } if( $TaskParameter['Timeout'] ) { $optionalParam['Timeout'] = $TaskParameter['Timeout'] } if( $TaskParameter['Overwrite'] ) { $optionalParam['Force'] = $TaskParameter['Overwrite'] | ConvertFrom-WhiskeyYamlScalar } Write-WhiskeyInfo -Context $TaskContext -Message "${Url} ${feedName}" foreach( $package in $packages ) { Write-WhiskeyInfo -Context $TaskContext -Message " $($package | Resolve-WhiskeyRelativePath)" Publish-ProGetUniversalPackage -Session $session -FeedName $feedName -PackagePath $package @optionalParam -ErrorAction Stop } } function Set-WhiskeyVariable { [CmdletBinding()] [Whiskey.Task('SetVariable',SupportsClean,SupportsInitialize)] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState foreach( $key in $TaskParameter.Keys ) { if( $key -match '^WHISKEY_' ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Variable ''{0}'' is a built-in Whiskey variable and can not be changed.' -f $key) continue } Add-WhiskeyVariable -Context $TaskContext -Name $key -Value $TaskParameter[$key] } } function Set-WhiskeyVariableFromPowerShellDataFile { [CmdletBinding()] [Whiskey.Task('SetVariableFromPowerShellDataFile')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')] [String]$Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $data = Import-PowerShellDataFile -Path $Path if( -not $data ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Path' -Message ('Failed to parse PowerShell Data File "{0}". Make sure this is a properly formatted PowerShell data file. Use the `Import-PowerShellDataFile` cmdlet.' -f $Path) return } function Set-VariableFromData { param( [Object]$Variable, [hashtable]$Data, [String]$ParentPropertyName = '' ) foreach( $propertyName in $Variable.Keys ) { $variableName = $Variable[$propertyName] if( -not $Data.ContainsKey($propertyName) ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Variables' -Message ('PowerShell Data File "{0}" does not contain "{1}{2}" property.' -f $Path,$ParentPropertyName,$propertyName) continue } $variableValue = $Data[$propertyName] if( $variableName | Get-Member 'Keys' ) { Set-VariableFromData -Variable $variableName -Data $variableValue -ParentPropertyName ('{0}{1}.' -f $ParentPropertyName,$propertyName) continue } Add-WhiskeyVariable -Context $TaskContext -Name $variableName -Value $variableValue } } Set-VariableFromData -Variable $TaskParameter['Variables'] -Data $data } function Set-WhiskeyVariableFromXml { [Whiskey.Task('SetVariableFromXml')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')] [String]$Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-WhiskeyVerbose -Context $TaskContext -Message ($Path) [xml]$xml = $null try { $xml = Get-Content -Path $Path -Raw } catch { $Global:Error.RemoveAt(0) Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Exception reading XML from file "{0}": {1}"' -f $Path,$_) return } $nsManager = New-Object -TypeName 'Xml.XmlNamespaceManager' -ArgumentList $xml.NameTable $prefixes = $TaskParameter['NamespacePrefixes'] if( $prefixes -and ($prefixes | Get-Member 'Keys') ) { foreach( $prefix in $prefixes.Keys ) { $nsManager.AddNamespace($prefix, $prefixes[$prefix]) } } $allowMissingNodes = $TaskParameter['AllowMissingNodes'] | ConvertFrom-WhiskeyYamlScalar $variables = $TaskParameter['Variables'] if( $variables -and ($variables | Get-Member 'Keys') ) { foreach( $variableName in $variables.Keys ) { $xpath = $variables[$variableName] $value = $xml.SelectNodes($xpath, $nsManager) | ForEach-Object { if( $_ | Get-Member 'InnerText' ) { $_.InnerText } elseif( $_ | Get-Member '#text' ) { $_.'#text' } } $exists = ' ' if( $value -eq $null ) { if( -not $allowMissingNodes ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Variable {0}: XPath expression "{1}" matched no elements/attributes in XML file "{2}".' -f $variableName,$xpath,$Path) return } $value = '' $exists = '!' } Write-WhiskeyVerbose -Context $TaskContext -Message (' {0} {1}' -f $exists,$xpath) Write-WhiskeyVerbose -Context $TaskContext -Message (' {0} = {1}' -f $variableName,($value | Select-Object -First 1)) $value | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0} {1}' -f (' ' * $variableName.Length),$_) } Add-WhiskeyVariable -Context $TaskContext -Name $variableName -Value $value } } } function Set-WhiskeyTaskDefaults { [CmdletBinding()] [Whiskey.Task('TaskDefaults',SupportsClean,SupportsInitialize)] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState foreach ($taskName in $TaskParameter.Keys) { foreach ($propertyName in $TaskParameter[$taskName].Keys) { Add-WhiskeyTaskDefault -Context $TaskContext -TaskName $taskName -PropertyName $propertyname -Value $TaskParameter[$taskName][$propertyName] -Force } } } function Set-WhiskeyVersion { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingPlainTextForPassword', '')] [Whiskey.Task('Version')] [Whiskey.RequiresPowerShellModule('ProGetAutomation', Version='3.*', VersionParameterName='ProGetAutomationVersion', ModuleInfoParameterName='ProGetAutomationModuleInfo')] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory)] [hashtable] $TaskParameter, [Whiskey.Tasks.ValidatePath(PathType='File')] [String] $Path, [String] $NuGetPackageID, [Uri] $UPackFeedUrl, [Uri] $ProGetUrl, [String] $UPackFeedName, [String] $UPackFeedCredentialID, [String] $UPackFeedApiKeyID, [String] $UPackGroupName, [String] $UPackName, [switch] $IncrementPatchVersion, [switch] $IncrementPrereleaseVersion ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState function ConvertTo-SemVer { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] $InputObject, $PropertyName, $VersionSource ) process { [SemVersion.SemanticVersion]$semver = $null if( -not [SemVersion.SemanticVersion]::TryParse($InputObject, [ref]$semver) ) { if( $VersionSource ) { $VersionSource = ' ({0})' -f $VersionSource } $optionalParam = @{ } if( $PropertyName ) { $optionalParam['PropertyName'] = $PropertyName } $msg = """$($InputObject)""$($VersionSource) is not a semantic version. See https://semver.org for " + 'documentation on semantic versions.' Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg @optionalParam return } return $semver } } [int]$nextPrereleaseVersion = 1 [Whiskey.BuildVersion]$buildVersion = $TaskContext.Version [SemVersion.SemanticVersion]$semver = $buildVersion.SemVer2 [String[]] $versions = @() [bool] $skipPackageLookup = -not $IncrementPatchVersion -and -not $IncrementPrereleaseVersion if( $TaskParameter[''] ) { $rawVersion = $TaskParameter[''] $semVer = $rawVersion | ConvertTo-SemVer -PropertyName 'Version' } elseif( $TaskParameter['Version'] ) { $rawVersion = $TaskParameter['Version'] $semVer = $rawVersion | ConvertTo-SemVer -PropertyName 'Version' } else { if( $Path ) { $fileInfo = Get-Item -Path $Path if( $fileInfo.Extension -eq '.psd1' ) { $moduleManifest = Test-ModuleManifest -Path $Path -ErrorAction Ignore -WarningAction Ignore $rawVersion = $moduleManifest.Version if( -not $rawVersion ) { $msg = "Unable to read version from PowerShell module manifest ""$($Path)"": the manifest is invalid " + 'or doesn''t contain a "ModuleVersion" property.' Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $nextPrerelease = '' if( ($moduleManifest | Get-Member -Name 'Prerelease') ) { $nextPrerelease = $moduleManifest.Prerelease } elseif( $moduleManifest.PrivateData -and ` $moduleManifest.PrivateData.ContainsKey('PSData') -and ` $moduleManifest.PrivateData['PSData'].ContainsKey('Prerelease') ) { $nextPrerelease = $moduleManifest.PrivateData['PSData']['Prerelease'] } if( $nextPrerelease ) { $rawVersion = "$($rawVersion)-$($nextPrerelease)" } $msg = "Read version ""$($rawVersion)"" from PowerShell module manifest ""$($Path)""." Write-WhiskeyVerbose -Context $TaskContext -Message $msg $semver = $rawVersion | ConvertTo-SemVer -VersionSource "from PowerShell module manifest ""$($Path)""" if( -not $skipPackageLookup ) { $msg = "Retrieving versions for PowerShell module $($moduleManifest.Name)." Write-WhiskeyVerbose -Context $TaskContext -Message $msg $allowPrereleaseArg = Get-AllowPrereleaseArg -CommandName 'Find-Module' -AllowPrerelease $versions = Find-Module -Name $moduleManifest.Name -AllVersions @allowPrereleaseArg -ErrorAction Ignore | Select-Object -ExpandProperty 'Version' } } elseif( $fileInfo.Name -eq 'package.json' ) { $npmPackage = [pscustomobject]::New() try { $npmPackage = Get-Content -Path $Path -Raw | ConvertFrom-Json } catch { $msg = "Node package.json file ""$($Path)"" contains invalid JSON." Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $rawVersion = $npmPackage | Select-Object -ExpandProperty 'Version' -ErrorAction Ignore if( -not $rawVersion ) { $msg = "Unable to read version from Node package.json ""$($Path)"": the ""Version"" property is " + 'missing.' Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $msg = "Read version ""$($rawVersion)"" from Node package.json ""$($Path)""." Write-WhiskeyVerbose -Context $TaskContext -Message $msg $semVer = $rawVersion | ConvertTo-SemVer -VersionSource "from Node package.json file ""$($Path)""" $pkgName = $npmPackage | Select-Object -ExpandProperty 'name' -ErrorAction Ignore if( $pkgName -and -not $skipPackageLookup ) { $msg = "Retrieving versions for NPM package $($pkgName)." Write-WhiskeyVerbose -Context $TaskContext -Message $msg Install-WhiskeyNode -InstallRootPath $TaskContext.BuildRoot ` -OutFileRootPath $TaskContext.OutputDirectory $packageVersions = Invoke-WhiskeyNpmCommand -Name 'show' ` -ArgumentList @($pkgName, 'versions', '--json') ` -BuildRoot $TaskContext.BuildRoot ` -ForDeveloper:($TaskContext.ByDeveloper) ` -ErrorAction Ignore 2>$null | ConvertFrom-Json if ($packageVersions | Get-Member -Name 'error') { $errCode = $packageVersions.error | Select-Object -ExpandProperty 'code' -ErrorAction 'Ignore' $errSummary = $packageVersions.error | Select-Object -ExpandProperty 'summary' -ErrorAction 'Ignore' if ($errCode -eq 'E404') { $msg = "NPM package ""${pkgName}"" has never been published to the registry. No existing versions." } else { $msg = "Failed to retrieve versions for NPM package ""${pkgName}"": [${errCode}] ${errSummary}" } Write-WhiskeyVerbose -Context $TaskContext -Message $msg $IncrementPrereleaseVersion = $false $versions = $semver } else { $versions = $packageVersions } } } elseif( $fileInfo.Extension -eq '.csproj' ) { [xml]$csprojXml = $null try { $csprojxml = Get-Content -Path $Path -Raw } catch { $msg = ".NET .csproj file ""$($Path)"" contains invalid XMl." Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } if( $csprojXml.DocumentElement.Attributes['xmlns'] ) { $msg = ".NET .csproj file ""$($Path)"" has an ""xmlns"" attribute. .NET Core/Standard .csproj " + 'files should not have a default namespace anymore ' + '(see https://docs.microsoft.com/en-us/dotnet/core/migration/). Please remove the "xmlns" ' + 'attribute from the root "Project" document element. If this is a .NET framework .csproj, it ' + 'doesn''t support versioning. Use the Whiskey Version task''s Version property to version ' + 'your assemblies.' Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $csprojVersionNode = $csprojXml.SelectSingleNode('/Project/PropertyGroup/Version') if( -not $csprojVersionNode ) { $msg = "Element ""/Project/PropertyGroup/Version"" does not exist in .NET .csproj file ""$($Path)"". " + 'Please create this element and set it to the MAJOR.MINOR.PATCH version of the next version ' + 'of your assembly.' Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $rawVersion = $csprojVersionNode.InnerText $msg = "Read version ""$($rawVersion)"" from .csproj file ""$($Path)"".'" Write-WhiskeyVerbose -Context $TaskContext -Message $msg $semver = $rawVersion | ConvertTo-SemVer -VersionSource "from .csproj file ""$($Path)""" if( -not $skipPackageLookup ) { if( -not $NuGetPackageID ) { $node = $csprojXml.SelectSingleNode('/Project/PropertyGroup/PackageId') if( $node ) { $NuGetPackageID = $node.InnerText } } if( $NuGetPackageID ) { $msg = "Retrieving versions for NuGet package ""$($NuGetPackageID)""." Write-WhiskeyVerbose -Context $TaskContext -Message $msg $allowPrereleaseArg = Get-AllowPrereleaseArg -CommandName 'Find-Package' -AllowPrerelease $versions = Find-Package -Name $NuGetPackageID -ProviderName 'NuGet' -AllVersions @allowPrereleaseArg | Select-Object -ExpandProperty 'Version' } } } elseif( $fileInfo.Name -eq 'metadata.rb' ) { $metadataContent = Get-Content -Path $Path -Raw $metadataContent = $metadataContent.Split([Environment]::NewLine) | Where-Object { $_ -ne '' } $rawVersion = $null foreach( $line in $metadataContent ) { if( $line -match '^\s*version\s+[''"](\d+\.\d+\.\d+)[''"]' ) { $rawVersion = $Matches[1] break } } if( -not $rawVersion ) { $msg = "Unable to locate property ""version 'x.x.x'"" in metadata.rb file ""$($Path)""" Stop-WhiskeyTask -TaskContext $TaskContext -Message $msg return } $msg = "Read version ""$($rawVersion)"" from metadata.rb file ""$($Path)""." Write-WhiskeyVerbose -Context $TaskContext -Message $msg $semver = $rawVersion | ConvertTo-SemVer -VersionSource "from metadata.rb file ""$($Path)""" } } } if( -not $skipPackageLookup ) { if( $UPackName ) { $credArgs = @{} if ($UPackFeedCredentialID) { $credArgs['Credential'] = Get-WhiskeyCredential -Context $TaskContext ` -ID $UPackFeedCredentialID ` -PropertyName 'UPackFeedCredentialID' } if ($UPackFeedApiKeyID) { $credArgs['ApiKey'] = Get-WhiskeyApiKey -Context $TaskContext -ID $UPackFeedApiKeyID -PropertyName 'UPackFeedApiKeyID' } if ($UPackFeedUrl) { $msg = 'The "UPackFeedUrl" property is obsolete. Use the "ProGetUrl" and "UPackFeedName" properties ' + 'instead.' Write-WhiskeyWarning $msg $ProGetUrl = "$($UPackFeedUrl.Scheme)://$($UPackFeedUrl.Authority)" $UPackFeedName = $UPackFeedUrl.Segments[-1] } $pgSession = New-ProGetSession -Url $ProGetUrl @credArgs $groupArg = @{} if ($UPackGroupName) { $groupArg['GroupName'] = $UPackGroupName } $msg = "Retrieving versions for universal package $($UPackName)." Write-WhiskeyVerbose -Context $TaskContext -Message $msg $numErr = $Global:Error.Count try { $versions = Get-ProGetUniversalPackage -Session $pgSession ` -FeedName $UPackFeedName ` -Name $UPackName ` @groupArg | Select-Object -ExpandProperty 'versions' } catch { $versions = @() for( $idx = $Global:Error.Count ; $idx -gt $numErr ; --$idx ) { $Global:Error.RemoveAt(0) } } } elseif( $NuGetPackageID ) { $msg = "Retrieving versions for NuGet package ""$($NuGetPackageID)""." Write-WhiskeyVerbose -Context $TaskContext -Message $msg $allowPrereleaseArg = Get-AllowPrereleaseArg -CommandName 'Find-Package' -AllowPrerelease $versions = Find-Package -Name $NuGetPackageID -ProviderName 'NuGet' -AllVersions @allowPrereleaseArg | Select-Object -ExpandProperty 'Version' } } $nextPrerelease = $TaskParameter['Prerelease'] if( $nextPrerelease -isnot [String] ) { $foundLabel = $false foreach( $object in $nextPrerelease ) { foreach( $map in $object ) { if( -not ($map | Get-Member -Name 'Keys') ) { $msg = "Unable to find keys in ""[$($map.GetType().Name)]$($map)"". It looks like you're trying " + 'use the Prerelease property to map branches to prerelease versions. If you want a static ' + "prerelease version, the syntax should be: Build: - Version: Prerelease: $($map) If you want certain branches to always have certain prerelease versions, set Prerelease to a list of key/value pairs: Build: - Version: Prerelease: - feature/*: alpha.`$(WHISKEY_PRERELEASE_VERSION) - develop: beta.`$(WHISKEY_PRERELEASE_VERSION) " Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Prerelease' -Message $msg return } $buildInfo = $TaskContext.BuildMetadata $branch = $buildInfo.ScmBranch if( $buildInfo.IsPullRequest ) { $branch = $buildInfo.ScmSourceBranch } foreach( $wildcardPattern in $map.Keys ) { if( $branch -like $wildcardPattern ) { Write-WhiskeyVerbose -Context $TaskContext -Message "$($branch) -like $($wildcardPattern)" $foundLabel = $true $nextPrerelease = $map[$wildcardPattern] break } else { Write-WhiskeyVerbose -Context $TaskContext -Message "$($branch) -notlike $($wildcardPattern)" } } if( $foundLabel ) { break } } if( $foundLabel ) { break } } if( -not $foundLabel ) { $nextPrerelease = '' } } if( $nextPrerelease ) { $buildSuffix = '' if( $semver.Build ) { $buildSuffix = '+{0}' -f $semver.Build } $rawVersion = '{0}.{1}.{2}-{3}{4}' -f $semver.Major,$semver.Minor,$semver.Patch,$nextPrerelease,$buildSuffix if( -not [SemVersion.SemanticVersion]::TryParse($rawVersion,[ref]$semver) ) { $msg = """$($nextPrerelease)"" is not a valid prerelease version. Only letters, numbers, hyphens, and " + 'periods are allowed. See https://semver.org for full documentation.' Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Prerelease' -Message $msg return } } if( $semver.Prerelease -match '(\d+)' ) { $nextPrereleaseVersion = $Matches[1] } else { $nextPrereleaseVersion = 1 } if( $versions ) { [SemVersion.SemanticVersion[]] $semVersions = $versions | ConvertTo-SemVer -ErrorAction Ignore | ForEach-Object { if ($_.Prerelease -notmatch '^([A-Za-z-]+)(\d+)$') { return $_ } $nextPrerelease = "$($Matches[1]).$($Matches[2])" return [SemVersion.SemanticVersion]::New($_.Major, $_.Minor, $_.Patch, $nextPrerelease, $_.Build) } $sortedSemVersions = [Collections.Generic.SortedSet[SemVersion.SemanticVersion]]::New($semversions) $semVersions = [SemVersion.SemanticVersion[]]::New($sortedSemVersions.Count) $sortedSemVersions.CopyTo($semVersions) [Array]::Reverse($semVersions) $semVersions | Write-WhiskeyDebug -Context $TaskContext if( $IncrementPatchVersion ) { $patchVersion = 0 $baseMajorMinorVersion = @($semver.Major,$semver.Minor) -join '.' $lastVersion = $semVersions | Where-Object { (@($_.Major,$_.Minor) -join '.') -eq $baseMajorMinorVersion } | Select-Object -First 1 if( $lastVersion ) { $patchVersion = $lastVersion.Patch + 1 } $nextPrerelease = $semver.Prerelease -replace '\d+', 1 $semver = [SemVersion.SemanticVersion]::New($semver.Major, $semver.Minor, $patchVersion, $nextPrerelease, $semver.Build) } if ($IncrementPrereleaseVersion -and $semver.Prerelease) { $baseVersion = @($semver.Major, $semver.Minor, $semver.Patch) -join '.' $prereleaseIdentifier = $semver.Prerelease -replace '[^A-Za-z]', '' $lastVersion = $semVersions | Where-Object { (@($_.Major,$_.Minor,$_.Patch) -join '.') -eq $baseVersion } | Where-Object { ($_.Prerelease -replace '[^A-Za-z]', '') -eq $prereleaseIdentifier } | Select-Object -First 1 $nextPrereleaseVersion = 1 if ($lastVersion -and $lastVersion.Prerelease -match '(\d+)') { $nextPrereleaseVersion = [int]$Matches[1] $nextPrereleaseVersion += 1 } $nextPrerelease = "$($semver.Prerelease).${nextPrereleaseVersion}" if ($semver.Prerelease -match '\d+') { $nextPrerelease = $semver.Prerelease -replace '\d+', $nextPrereleaseVersion } $semver = [SemVersion.SemanticVersion]::New($semver.Major, $semver.Minor, $semver.Patch, $nextPrerelease, $semver.Build) } } $build = $TaskParameter['Build'] if( $build ) { $prereleaseSuffix = '' if( $semver.Prerelease ) { $prereleaseSuffix = '-{0}' -f $semver.Prerelease } $build = $build -replace '[^A-Za-z0-9\.-]', '-' $rawVersion = '{0}.{1}.{2}{3}+{4}' -f $semver.Major,$semver.Minor,$semver.Patch,$prereleaseSuffix,$build if( -not [SemVersion.SemanticVersion]::TryParse($rawVersion,[ref]$semver) ) { $msg = """$($build)"" is not valid build metadata. Only letters, numbers, hyphens, and periods are " + 'allowed. See https://semver.org for full documentation.' Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Build' -Message $msg return } } # Build metadata is only available when running under a build server. if( $TaskContext.ByDeveloper ) { $semver = New-Object -TypeName 'SemVersion.SemanticVersion' ` -ArgumentList $semver.Major,$semVer.Minor,$semVer.Patch,$semver.Prerelease } $buildVersion.SemVer2 = $semver Write-WhiskeyInfo -Context $TaskContext -Message "Building version $($semver)" $buildVersion.Version = [Version](@($semver.Major,$semver.Minor,$semver.Patch) -join '.') Write-WhiskeyVerbose -Context $TaskContext -Message "Version $($buildVersion.Version)" $buildVersion.SemVer2NoBuildMetadata = New-Object 'SemVersion.SemanticVersion' ($semver.Major,$semver.Minor,$semver.Patch,$semver.Prerelease) $msg = "SemVer2NoBuildMetadata $($buildVersion.SemVer2NoBuildMetadata)" Write-WhiskeyVerbose -Context $TaskContext -Message $msg $semver1Prerelease = $semver.Prerelease if( $semver1Prerelease ) { $semver1Prerelease = $semver1Prerelease -replace '[^A-Za-z0-9]','' } $buildVersion.SemVer1 = New-Object 'SemVersion.SemanticVersion' ($semver.Major,$semver.Minor,$semver.Patch,$semver1Prerelease) Write-WhiskeyVerbose -Context $TaskContext -Message "SemVer1 $($buildVersion.SemVer1)" } function New-WhiskeyZipArchive { [Whiskey.Task('Zip')] [Whiskey.RequiresPowerShellModule('Zip', Version='0.3.*', VersionParameterName='ZipVersion')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Parameter(Mandatory)] [hashtable]$TaskParameter, [Whiskey.Tasks.ValidatePath()] [String]$SourceRoot, [Whiskey.Tasks.ValidatePath(Mandatory,AllowNonexistent)] [String]$ArchivePath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState function Write-CompressionInfo { param( [Parameter(Mandatory)] [ValidateSet('file','directory','filtered directory')] [String]$What, [String]$Source, [String]$Destination ) if( $Destination ) { $Destination = ' -> {0}' -f ($Destination -replace '\\','/') } if( [IO.Path]::DirectorySeparatorChar -eq [IO.Path]::AltDirectorySeparatorChar ) { $Source = $Source -replace '\\','/' } Write-WhiskeyInfo -Context $TaskContext -Message (' compressing {0,-18} {1}{2}' -f $What,$Source,$Destination) } $behaviorParams = @{ } if( $TaskParameter['CompressionLevel'] ) { [IO.Compression.CompressionLevel]$compressionLevel = [IO.Compression.CompressionLevel]::NoCompression if( -not [Enum]::TryParse($TaskParameter['CompressionLevel'], [ref]$compressionLevel) ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'CompressionLevel' -Message ('Value "{0}" is an invalid compression level. Must be one of: {1}.' -f $TaskParameter['CompressionLevel'],([Enum]::GetValues([IO.Compression.CompressionLevel]) -join ', ')) return } $behaviorParams['CompressionLevel'] = $compressionLevel } if( $TaskParameter['EntryNameEncoding'] ) { $entryNameEncoding = $TaskParameter['EntryNameEncoding'] [int]$codePage = 0 if( [int]::TryParse($entryNameEncoding,[ref]$codePage) ) { try { $entryNameEncoding = [Text.Encoding]::GetEncoding($codePage) } catch { Write-Error -ErrorRecord $_ Stop-WhiskeyTask -TaskContext $TaskContext -Message ('EntryNameEncoding: An encoding with code page "{0}" does not exist. To get a list of encodings, run `[Text.Encoding]::GetEncodings()` or see https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding . Use the encoding''s `CodePage` or `WebName` property as the value of this property.' -f $entryNameEncoding) return } } else { try { $entryNameEncoding = [Text.Encoding]::GetEncoding($entryNameEncoding) } catch { Write-Error -ErrorRecord $_ Stop-WhiskeyTask -TaskContext $TaskContext -Message ('EntryNameEncoding: An encoding named "{0}" does not exist. To get a list of encodings, run `[Text.Encoding]::GetEncodings()` or see https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding . Use the encoding''s "CodePage" or "WebName" property as the value of this property.' -f $entryNameEncoding) return } } $behaviorParams['EntryNameEncoding'] = $entryNameEncoding } Write-WhiskeyInfo -Context $TaskContext -Message ('Creating ZIP archive "{0}".' -f $ArchivePath) $archiveDirectory = $ArchivePath | Split-Path -Parent if( $archiveDirectory -and -not (Test-Path -Path $archiveDirectory -PathType Container) ) { New-Item -Path $archiveDirectory -ItemType 'Directory' -Force | Out-Null } if( -not $TaskParameter['Path'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Path" is required. It must be a list of paths, relative to your whiskey.yml file, of files or directories to include in the ZIP archive.') return } New-ZipArchive -Path $ArchivePath @behaviorParams -Force | Out-Null if( $SourceRoot ) { Write-WhiskeyWarning -Context $TaskContext -Message ('The "SourceRoot" property is obsolete. Please use the "WorkingDirectory" property instead.') $ArchivePath = Resolve-Path -Path $ArchivePath | Select-Object -ExpandProperty 'ProviderPath' Push-Location -Path $SourceRoot } try { foreach( $item in $TaskParameter['Path'] ) { $override = $False if( (Get-Member -InputObject $item -Name 'Keys') ) { $sourcePath = $null $override = $True foreach( $key in $item.Keys ) { $destinationItemName = $item[$key] $sourcePath = $key } } else { $sourcePath = $item } $sourcePaths = $sourcePath | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' if( -not $sourcePaths ) { return } $basePath = (Get-Location).Path foreach( $sourcePath in $sourcePaths ) { $addParams = @{ BasePath = $basePath } $destinationParam = @{ } if( $override ) { $addParams = @{ EntryName = $destinationItemName } $destinationParam['Destination'] = $destinationItemName } if( (Test-Path -Path $sourcePath -PathType Leaf) ) { Write-CompressionInfo -What 'file' -Source $sourcePath @destinationParam Add-ZipArchiveEntry -ZipArchivePath $ArchivePath -InputObject $sourcePath @addParams @behaviorParams continue } function Find-Item { param( [Parameter(Mandatory)] $Path ) if( (Test-Path -Path $Path -PathType Leaf) ) { return Get-Item -Path $Path } $Path = Join-Path -Path $Path -ChildPath '*' & { Get-ChildItem -Path $Path -Include $TaskParameter['Include'] -Exclude $TaskParameter['Exclude'] -File Get-Item -Path $Path -Exclude $TaskParameter['Exclude'] | Where-Object { $_.PSIsContainer } } | ForEach-Object { if( $_.PSIsContainer ) { Find-Item -Path $_.FullName } else { $_ } } } if( $override ) { $overrideBasePath = Resolve-Path -Path $sourcePath | Select-Object -ExpandProperty 'ProviderPath' if( (Test-Path -Path $overrideBasePath -PathType Leaf) ) { $overrideBasePath = Split-Path -Parent -Path $overrideBasePath } $addParams['BasePath'] = $overrideBasePath $addParams['EntryParentPath'] = $destinationItemName $addParams.Remove('EntryName') $destinationParam['Destination'] = $destinationItemName } $typeDesc = 'directory' if( $TaskParameter['Include'] -or $TaskParameter['Exclude'] ) { $typeDesc = 'filtered directory' } Write-CompressionInfo -What $typeDesc -Source $sourcePath @destinationParam Find-Item -Path $sourcePath | Add-ZipArchiveEntry -ZipArchivePath $ArchivePath @addParams @behaviorParams } } } finally { if( $SourceRoot ) { Pop-Location } } } |