Whiskey.psm1
$startedAt = Get-Date function Write-Timing { param( [Parameter(Position=0)] $Message ) $now = Get-Date Write-Debug -Message ('[{0}] [{1}] {2}' -f $now,($now - $startedAt),$Message) } $events = @{ } $powerShellModulesDirectoryName = 'PSModules' $whiskeyScriptRoot = $PSScriptRoot $whiskeyModulesRoot = Join-Path -Path $whiskeyScriptRoot -ChildPath 'Modules' -Resolve $whiskeyBinPath = Join-Path -Path $whiskeyScriptRoot -ChildPath 'bin' -Resolve $whiskeyNuGetExePath = Join-Path -Path $whiskeyBinPath -ChildPath 'NuGet.exe' -Resolve $buildStartedAt = [DateTime]::MinValue $PSModuleAutoLoadingPreference = 'None' $supportsWriteInformation = Get-Command -Name 'Write-Information' -ErrorAction Ignore 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.' $attr = New-Object -TypeName 'Whiskey.TaskAttribute' -ArgumentList 'Whiskey' -ErrorAction Ignore if( -not ($attr | Get-Member 'Platform') ) { Write-Error -Message ('You''ve got an old version of Whiskey loaded. Please open a new PowerShell session.') -ErrorAction Stop } function Assert-Member { [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Object, [Parameter(Mandatory)] [string[]] $Property ) foreach( $propertyToCheck in $Property ) { if( -not ($Object | Get-Member $propertyToCheck) ) { Write-Debug -Message ('Object "{0}" is missing member "{1}".' -f $Object.GetType().FullName,$propertyToCheck) Write-Error -Message ('You''ve got an old version of Whiskey loaded. Please open a new PowerShell session.') -ErrorAction Stop } } } $context = New-Object -TypeName 'Whiskey.Context' Assert-Member -Object $context -Property @( 'TaskPaths', 'MSBuildConfiguration', 'ApiKeys' ) $taskAttribute = New-Object -TypeName 'Whiskey.TaskAttribute' -ArgumentList 'Fubar' Assert-Member -Object $taskAttribute -Property @( 'Aliases', 'WarnWhenUsingAlias', 'Obsolete', 'ObsoleteMessage' ) [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 ('You''ve got an old version of Whiskey loaded. Please open a new PowerShell session.') -ErrorAction Stop } Write-Timing ('Creating internal module variables.') # PowerShell 5.1 doesn't have these variables so create them if they don't exist. if( -not (Get-Variable -Name 'IsLinux' -ErrorAction Ignore) ) { $IsLinux = $false $IsMacOS = $false $IsWindows = $true } $dotNetExeName = 'dotnet' $nodeExeName = 'node' $nodeDirName = 'bin' if( $IsWindows ) { $dotNetExeName = '{0}.exe' -f $dotNetExeName $nodeExeName = '{0}.exe' -f $nodeExeName $nodeDirName = '' } $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=$true)] [object] # The context of the build that needs the API key. $Context, [Parameter(Mandatory=$true)] [string] # 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. $ID, [Parameter(Mandatory=$true)] # 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=$true)] [object] # The context of the build that needs the API key. $Context, [Parameter(Mandatory=$true)] [string] # 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. $ID, [Parameter(Mandatory=$true)] [pscredential] # The value of the credential. $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=$true,ValueFromPipeline=$true)] [object] # The current build context. Use `New-WhiskeyContext` to create context objects. $Context, [Parameter(Mandatory=$true)] [string] # The name of the task that a default parameter value will be set. $TaskName, [Parameter(Mandatory=$true)] [string] # The name of the task parameter to set a default value for. $PropertyName, [Parameter(Mandatory=$true)] # The default value for the task parameter. $Value, [switch] # Overwrite an existing task default with a new value. $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=$true)] [object] # The context of the build here you want to add a variable. $Context, [Parameter(Mandatory=$true)] [string] # The name of the variable. $Name, [Parameter(Mandatory=$true)] [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=$true)] [string] # The path to check. $Path, [string] # The path to a command inside the module path. $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-Error -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-Error -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=$true)] [string] # The path to check. $Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not (Test-Path -Path $Path -PathType Leaf) ) { Write-Error -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 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)] [Whiskey.Context] # The context to convert. You can pass an existing context via the pipeline. $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=$true,ValueFromPipeline=$true)] [AllowEmptyString()] [AllowNull()] [string] # The object to convert. $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-Error -Message ('Unable to convert scalar value ''{0}''. See http://yaml.org/type/ for documentation on YAML''s scalars.' -f $InputObject) -ErrorAction $ErrorActionPreference } } 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)] [object] # The context to convert. You can pass an existing context via the pipeline. $InputObject ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState try { function Sync-ObjectProperty { param( [Parameter(Mandatory=$true)] [object] $Source, [Parameter(Mandatory=$true)] [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-Debug ('{0} {1} -> {2}' -f $_,$Destination.$_,$Source.$_) $propertyType = $destinationType.GetProperty($_).PropertyType if( $propertyType.IsSubclassOf([IO.FileSystemInfo]) ) { if( $Source.$_ ) { Write-Debug -Message ('{0} {1} = {2}' -f $propertyType.FullName,$_,$Source.$_) $Destination.$_ = New-Object $propertyType.FullName -ArgumentList $Source.$_.FullName } } else { $Destination.$_ = $Source.$_ } } Write-Debug ('Source -eq $null ? {0}' -f ($Source -eq $null)) if( $Source -ne $null ) { Write-Debug -Message 'Source' Get-Member -InputObject $Source | Out-String | Write-Debug } Write-Debug ('Destination -eq $null ? {0}' -f ($Destination -eq $null)) if( $Destination -ne $null ) { Write-Debug -Message 'Destination' Get-Member -InputObject $Destination | Out-String | Write-Debug } 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-Debug ('Destination.{0,-20} -eq $null ? {1}' -f $name,($value -eq $null)) Write-Debug (' .{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-Debug -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-Debug (' [{0,-20}] -> {1}' -f $key,$value) $destinationObject[$key] = $sourceObject[$key] } } elseif( $destinationObject -is [Collections.IList] ) { $idx = 0 foreach( $item in $sourceObject ) { Write-Debug(' [{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-Debug 'Variables' $context.Variables | ConvertTo-Json -Depth 50 | Write-Debug Write-Debug 'ApiKeys' $context.ApiKeys | ConvertTo-Json -Depth 50 | Write-Debug Write-Debug 'Credentials' $context.Credentials | ConvertTo-Json -Depth 50 | Write-Debug Write-Debug 'TaskDefaults' $context.TaskDefaults | ConvertTo-Json -Depth 50 | Write-Debug Write-Debug 'TaskPaths' $context.TaskPaths | ConvertTo-Json | Write-Debug 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=$true)] [object] # The object to convert to a semantic version. Can be a version string, number, or date/time. $InputObject ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState [version]$asVersion = $null if( $InputObject -is [string] ) { [int]$asInt = 0 [double]$asDouble = 0.0 [SemVersion.SemanticVersion]$semVersion = $null if( [SemVersion.SemanticVersion]::TryParse($InputObject,[ref]$semVersion) ) { return $semVersion } if( [version]::TryParse($InputObject,[ref]$asVersion) ) { $InputObject = $asVersion } elseif( [int]::TryParse($InputObject,[ref]$asInt) ) { $InputObject = $asInt } } 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() } } else { Write-Error -Message ('Unable to convert ''{0}'' to a semantic version. We tried parsing it as a version, date, double, and integer. Sorry. But we''re giving up.' -f $PSBoundParameters['InputObject']) -ErrorAction $ErrorActionPreference return } $semVersion = $null if( ([SemVersion.SemanticVersion]::TryParse($InputObject,[ref]$semVersion)) ) { return $semVersion } Write-Error -Message ('Unable to convert ''{0}'' of type ''{1}'' to a semantic version.' -f $PSBoundParameters['InputObject'],$PSBoundParameters['InputObject'].GetType().FullName) -ErrorAction $ErrorActionPreference } } 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=$true)] [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-Error -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 Format-WhiskeyMessage { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context]$Context, [Parameter(Mandatory=$true,ValueFromPipeline=$true)] [AllowEmptyString()] [AllowNull()] [string]$Message, [int]$Indent = 0 ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState ('[{0}][{1}][{2}] {3}{4}' -f $Context.PipelineName,$Context.TaskIndex,$Context.TaskName,(' ' * ($Indent * 2)),$Message) } 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 ) { return '' } $path = Join-Path -Path $toolsPath -ChildPath 'MSBuild.exe' if( (Test-Path -Path $path -PathType Leaf) ) { return $path } return '' } filter Test-Version { param( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] $InputObject ) [version]$version = $null [version]::TryParse($InputObject,[ref]$version) } $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) ) { 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 = '' } } [pscustomobject]@{ Name = $name; Version = [version]$name; Path = $msbuildPath; Path32 = $msbuildPath32; } } foreach( $instance in (Get-VSSetupInstance) ) { $msbuildRoot = Join-Path -Path $instance.InstallationPath -ChildPath 'MSBuild' if( -not (Test-Path -Path $msbuildRoot -PathType Container) ) { Write-Verbose -Message ('Skipping {0} {1}: its MSBuild directory ''{2}'' doesn''t exist.' -f $instance.DisplayName,$instance.InstallationVersion,$msbuildRoot) continue } $versionRoots = Get-ChildItem -Path $msbuildRoot -Directory | Where-Object { Test-Version $_.Name } foreach( $versionRoot in $versionRoots ) { $path = Join-Path -Path $versionRoot.FullName -ChildPath 'Bin\amd64\MSBuild.exe' $path32 = Join-Path -Path $versionRoot.FullName -ChildPath 'Bin\MSBuild.exe' if( -not (Test-Path -Path $path -PathType Leaf) ) { $path = $path32 } if( -not (Test-Path -Path $path -PathType Leaf) ) { continue } if( -not (Test-Path -Path $path32 -PathType Leaf) ) { $path32 = '' } [pscustomobject]@{ Name = $versionRoot.Name; Version = [version]$versionRoot.Name; Path = $path; Path32 = $path32; } } } } function Get-TaskParameter { [CmdletBinding()] param( [Parameter(Mandatory)] [string] # The name of the command. $Name, [Parameter(Mandatory)] [hashtable] # The properties from the tasks's YAML. $TaskProperty, [Parameter(Mandatory)] [Whiskey.Context] # The current 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. $taskParameters = @{ } [Management.Automation.ParameterMetadata]$cmdParameter = $null foreach( $cmdParameter in $cmdParameters.Values ) { $propertyName = $cmdParameter.Name $value = $TaskProperty[$propertyName] # 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]$validateAsPathAttr = $cmdParameter.Attributes | Where-Object { $_ -is [Whiskey.Tasks.ValidatePathAttribute] } if( $validateAsPathAttr ) { $optionalParams = @{ } if( $validateAsPathAttr.PathType ) { $optionalParams['PathType'] = $validateAsPathAttr.PathType } if( $value ) { $value = $value | Resolve-WhiskeyTaskPath -TaskContext $Context -PropertyName $propertyName @optionalParams } if( -not $value -and $validateAsPathAttr.Mandatory ) { $errorMsg = 'path "{0}" does not exist.' -f $TaskProperty[$propertyName] if( -not $TaskProperty[$propertyName] ) { $errorMsg = 'is mandatory.' } Stop-WhiskeyTask -TaskContext $Context -PropertyName $cmdParameter.Name -Message $errorMsg } $expectedPathType = $validateAsPathAttr.PathType if( $value -and $expectedPathType ) { $pathType = 'Leaf' if( $expectedPathType -eq 'Directory' ) { $pathType = 'Container' } $invalidPaths = $value | Where-Object { -not (Test-Path -Path $_ -PathType $pathType) } if( $invalidPaths ) { Stop-WhiskeyTask -TaskContext $Context -PropertyName $cmdParameter.Name -Message (@' must be a {0}, but found {1} path(s) that are not: * {2} '@ -f $expectedPathType.ToLowerInvariant(),($invalidPaths | Measure-Object).Count,($invalidPaths -join ('{0}* ' -f [Environment]::NewLine))) } } $pathCount = $value | Measure-Object | Select-Object -ExpandProperty 'Count' if( $cmdParameter.ParameterType -ne ([string[]]) -and $pathCount -gt 1 ) { Stop-WhiskeyTask -TaskContext $Context -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,$TaskProperty[$propertyName],$pathCount,$task.CommandName,($value -join ('{0}* ' -f [Environment]::NewLine))) } } # If the user didn't provide a value and we couldn't find one, don't pass anything. if( -not $TaskProperty.ContainsKey($propertyName) -and -not $value ) { continue } $taskParameters[$propertyName] = $value $TaskProperty.Remove($propertyName) } foreach( $name in @( 'TaskContext', 'Context' ) ) { if( $cmdParameters.ContainsKey($name) ) { $taskParameters[$name] = $Context } } foreach( $name in @( 'TaskParameter', 'Parameter' ) ) { if( $cmdParameters.ContainsKey($name) ) { $taskParameters[$name] = $TaskProperty } } return $taskParameters } 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=$true)] [object] # The current build context. Use `New-WhiskeyContext` to create context objects. $Context, [Parameter(Mandatory=$true)] [string] # The ID of the API key. You make this up. $ID, [Parameter(Mandatory=$true)] [string] # 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. $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] $stringPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) return [Runtime.InteropServices.Marshal]::PtrToStringAuto($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' function Get-EnvironmentVariable { param( $Name ) Get-Item -Path ('env:{0}' -f $Name) | 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 } 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 { Write-Error -Message ('Unsupported AppVeyor source control provider ''{0}''. 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.' -f $_) } } $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' $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-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=$true)] [object] # The current build context. Use `New-WhiskeyContext` to create context objects. $Context, [Parameter(Mandatory=$true)] [string] # The ID of the credential. You make this up. $ID, [Parameter(Mandatory=$true)] [string] # 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. $PropertyName, [string] # INTERNAL. DO NOT USE. $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=$true)] [Whiskey.Context] # The context of the current build. $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-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( [Switch] # Return tasks that are obsolete. Otherwise, no obsolete tasks are returned. $Force ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState [Management.Automation.FunctionInfo]$functionInfo = $null; foreach( $functionInfo in (Get-Command -CommandType Function) ) { $functionInfo.ScriptBlock.Attributes | Where-Object { $_ -is [Whiskey.TaskAttribute] } | ForEach-Object { $_.CommandName = $functionInfo.Name $_ } | Where-Object { if( $Force ) { $true } return -not $_.Obsolete } } } 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 a module with the same name is currently loaded, it is removed and re-imported. The module must be installed in Whiskey's PowerShell modules directory. Use the `RequiresTool` attribute on a task to have Whiskey install a module in this directory or the `GetPowerShellModule` task to install a module in the appropriate place. Pass the name of the modules to the `Name` parameter. .EXAMPLE Import-WhiskeyPowerShellModule -Name 'BuildMasterAutomtion' Demonstrates how to use this method to import a single module. .EXAMPLE Import-WhiskeyPowerShellModule -Name 'BuildMasterAutomtion','ProGetAutomation' Demonstrates how to use this method to import multiple modules. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string[]] # The module names to import. $Name ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState & { $VerbosePreference = 'SilentlyContinue' Get-Module -Name $Name | Remove-Module -Force -WhatIf:$false } $searchPaths = & { Join-Path -Path (Get-Location).ProviderPath -ChildPath $powerShellModulesDirectoryName $whiskeyModulesRoot } foreach( $moduleName in $Name ) { foreach( $searchDir in $searchPaths ) { $moduleDir = Join-Path -Path $searchDir -ChildPath $moduleName if( (Test-Path -Path $moduleDir -PathType Container) ) { Write-Debug -Message ('PSModuleAutoLoadingPreference = "{0}"' -f $PSModuleAutoLoadingPreference) Write-Verbose -Message ('Import PowerShell module "{0}" from "{1}".' -f $moduleName,$searchDir) $numErrorsBefore = $Global:Error.Count & { $VerbosePreference = 'SilentlyContinue' Import-Module -Name $moduleDir -Global -Force -ErrorAction Stop } 4> $null # Some modules (...cough...PowerShellGet...cough...) write silent errors during import. This causes our tests # to fail. I know this is a little extreme. $numErrorsAfter = $Global:Error.Count - $numErrorsBefore for( $idx = 0; $idx -lt $numErrorsAfter; ++$idx ) { $Global:Error.RemoveAt(0) } break } } if( -not (Get-Module -Name $moduleName) ) { Write-Error -Message ('Module "{0}" does not exist. Make sure your task uses the "RequiresTool" attribute so that the module gets installed automatically.' -f $moduleName) -ErrorAction Stop } } } function Import-WhiskeyYaml { [CmdletBinding()] param( [Parameter(Mandatory=$true,ParameterSetName='FromFile')] [string] $Path, [Parameter(Mandatory=$true,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 ) } finally { $reader.Close() } if( -not $config ) { $config = @{} } if( $config -is [string] ) { $config = @{ $config = '' } } return $config } function Install-WhiskeyDotNetSdk { <# .SYNOPSIS Installs the .NET Core SDK tooling. .DESCRIPTION The `Install-WhiskeyDotNetSdk` function will install the .NET Core SDK tools and return the path to the installed `dotnet.exe` command. If you specify the `Global` switch then the function will first look for any globally installed .NET Core SDK's with the desired version already installed. If one is found, then install is skipped and the path to the global install is returned. The function uses the `dotnet-install.ps1` script from the [dotnet-cli](https://github.com/dotnet/cli) GitHub repository to download and install the SDK. .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'. .EXAMPLE Install-WhiskeyDotNetSdk -InstallRoot 'C:\Build\.dotnet' -Version '2.1.4' -Global Demonstrates searching for an existing global install of the .NET Core SDK version '2.1.4'. If not found globally, the SDK will be installed to 'C:\Build\.dotnet'. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] # Directory where the .NET Core SDK will be installed. $InstallRoot, [Parameter(Mandatory=$true)] [string] # Version of the .NET Core SDK to install. $Version, [switch] # Search for the desired version from existing global installs of the .NET Core SDK. If found, the install is skipped and the path to the global install is returned. $Global ) Set-StrictMode -version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if ($Global) { $dotnetGlobalInstalls = Get-Command -Name 'dotnet' -All -ErrorAction Ignore | Select-Object -ExpandProperty 'Path' if ($dotnetGlobalInstalls) { Write-Verbose -Message ('[{0}] Found global installs of .NET Core SDK: "{1}"' -f $MyInvocation.MyCommand,($dotnetGlobalInstalls -join '","')) Write-Verbose -Message ('[{0}] Checking global installs for SDK version "{1}"' -f $MyInvocation.MyCommand,$Version) foreach ($dotnetPath in $dotnetGlobalInstalls) { $sdkPath = Join-Path -Path ($dotnetPath | Split-Path -Parent) -ChildPath ('sdk\{0}' -f $Version) if (Test-Path -Path $sdkPath -PathType Container) { Write-Verbose ('[{0}] Found SDK version "{1}" at "{2}"' -f $MyInvocation.MyCommand,$Version,$sdkPath) return $dotnetPath } } } Write-Verbose -Message ('[{0}] .NET Core SDK version "{1}" not found globally' -f $MyInvocation.MyCommand,$Version) } $verboseParam = @{} if ($VerbosePreference -eq 'Continue') { $verboseParam['Verbose'] = $true } Write-Verbose -Message ('[{0}] Installing .NET Core SDK version "{1}" to "{2}"' -f $MyInvocation.MyCommand,$Version,$InstallRoot) $dotnetInstallScript = Join-Path -Path $whiskeyBinPath -ChildPath 'dotnet-install.ps1' -Resolve $errorActionParam = @{ ErrorAction = 'Stop' } $installingWithShell = $false $executableName = 'dotnet.exe' if( $IsLinux -or $IsMacOS ) { $dotnetInstallScript = Join-Path -Path $whiskeyBinPath -ChildPath 'dotnet-install.sh' -Resolve $errorActionParam = @{ } $installingWithShell = $true $executableName = 'dotnet' } Invoke-Command -NoNewScope -ArgumentList $dotnetInstallScript,$InstallRoot,$Version,$verboseParam -ScriptBlock { param( $dotnetInstall, $InstallDir, $VersionNumber, $Verbose ) $errCount = $Global:Error.Count & { if( $installingWithShell ) { Write-Verbose ('bash {0} -InstallDir "{1}" -Version "{2}" -NoPath' -f $dotnetInstall,$InstallDir,$VersionNumber) bash $dotnetInstall -InstallDir $InstallDir -Version $VersionNumber -NoPath } else { Write-Verbose ('{0} -InstallDir "{1}" -Version "{2}" -NoPath' -f $dotnetInstall,$InstallDir,$VersionNumber) & $dotnetInstall -InstallDir $InstallDir -Version $VersionNumber -NoPath @Verbose @errorActionParam } } | Write-Verbose -Verbose if( $installingWithShell -and $LASTEXITCODE ) { Write-Error -Message ('{0} exited with code "{1}". Failed to install .NET Core.' -f $dotnetInstall,$LASTEXITCODE) -ErrorAction Stop return } $newErrCount = $Global:Error.Count for( $count = 0; $count -lt $newErrCount; ++$count ) { $Global:Error.RemoveAt(0) } } $dotnetPath = Join-Path -Path $InstallRoot -ChildPath $executableName -Resolve -ErrorAction Ignore if (-not $dotnetPath) { Write-Error -Message ('After attempting to install .NET Core SDK version "{0}", the "{1}" executable was not found in "{2}"' -f $Version,$executableName,$InstallRoot) return } $sdkPath = Join-Path -Path $InstallRoot -ChildPath ('sdk\{0}' -f $Version) if (-not (Test-Path -Path $sdkPath -PathType Container)) { Write-Error -Message ('The "{0}" command was installed but version "{1}" of the SDK was not found at "{2}"' -f $executableName,$Version,$sdkPath) 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=$true)] [string] # Path where the `.dotnet` directory will be installed containing the .NET Core SDK. $InstallRoot, [Parameter(Mandatory=$true)] [string] # 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. $WorkingDirectory, [AllowEmptyString()] [AllowNull()] [string] # The version of the .NET Core SDK to install. Accepts wildcards. $Version ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $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 ($Version) { Write-Verbose -Message ('[{0}] .NET Core SDK version ''{1}'' found in whiskey.yml' -f $MyInvocation.MyCommand,$Version) $sdkVersion = Resolve-WhiskeyDotNetSdkVersion -Version $Version } elseif (Test-Path -Path $globalJsonPath -PathType Leaf) { try { $globalJson = Get-Content -Path $globalJsonPath -Raw | ConvertFrom-Json } catch { Write-Error -Message ('global.json file ''{0}'' contains invalid JSON.' -f $globalJsonPath) return } $globalJsonVersion = $globalJson | Select-Object -ExpandProperty 'sdk' -ErrorAction Ignore | Select-Object -ExpandProperty 'version' -ErrorAction Ignore if ($globalJsonVersion) { Write-Verbose -Message ('[{0}] .NET Core SDK version ''{1}'' found in ''{2}''' -f $MyInvocation.MyCommand,$globalJsonVersion,$globalJsonPath) $sdkVersion = Resolve-WhiskeyDotNetSdkVersion -Version $globalJsonVersion } } if (-not $sdkVersion) { Write-Verbose -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 } $dotnetPath = Install-WhiskeyDotNetSdk -InstallRoot (Join-Path -Path $InstallRoot -ChildPath '.dotnet') -Version $sdkVersion -Global Set-WhiskeyDotNetGlobalJson -Directory ($globalJsonPath | Split-Path -Parent) -SdkVersion $sdkVersion return $dotnetPath } function Install-WhiskeyNode { [CmdletBinding()] param( [Parameter(Mandatory)] [string] # The directory where Node should be installed. Will actually be installed into `Join-Path -Path $InstallRoot -ChildPath '.node'`. $InstallRoot, [Switch] # Are we running in clean mode? If so, don't re-install the tool. $InCleanMode, [string] # 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. $Version ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $InstallRoot -ErrorAction Ignore if( $InCleanMode ) { if( $nodePath ) { return $nodePath } return } $npmVersionToInstall = $null $nodeVersionToInstall = $null $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 $InstallRoot -ChildPath 'package.json' } if( (Test-Path -Path $packageJsonPath -PathType Leaf) ) { Write-Verbose -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-WhiskeyTool -Name 'Node' -InstallRoot $InstallRoot $installNode = $true } } else { $installNode = $true } $nodeRoot = Join-Path -Path $InstallRoot -ChildPath '.node' if( -not (Test-Path -Path $nodeRoot -PathType Container) ) { New-Item -Path $nodeRoot -ItemType 'Directory' -Force | Out-Null } $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 $nodeZipFile = Join-Path -Path $nodeRoot -ChildPath $filename if( -not (Test-Path -Path $nodeZipFile -PathType Leaf) ) { $uri = 'https://nodejs.org/dist/{0}/{1}' -f $nodeVersionToInstall.version,$filename try { Invoke-WebRequest -Uri $uri -OutFile $nodeZipFile } catch { $responseStatus = $_.Exception.Response.StatusCode $errorMsg = 'Failed to download Node {0}. Received a {1} ({2}) response when retreiving URI {3}.' -f $nodeVersionToInstall.version,$responseStatus,[int]$responseStatus,$uri if( $responseStatus -eq [Net.HttpStatusCode]::NotFound ) { $errorMsg = '{0} It looks like this version of Node wasn''t packaged as a ZIP file. Please use Node v4.5.0 or newer.' -f $errorMsg } throw $errorMsg } } if( $installNode ) { if( $IsWindows ) { # Windows/.NET can't handle the long paths in the Node package, so on that platform, we need to download 7-zip. It can handle paths that long. $7zipPackageRoot = Install-WhiskeyTool -NuGetPackageName '7-Zip.CommandLine' -DownloadRoot $InstallRoot $7z = Join-Path -Path $7zipPackageRoot -ChildPath 'tools\x64\7za.exe' -Resolve -ErrorAction Stop Write-Verbose -Message ('{0} x {1} -o{2} -y' -f $7z,$nodeZipFile,$nodeRoot) & $7z 'x' $nodeZipFile ('-o{0}' -f $nodeRoot) '-y' | Write-Verbose Get-ChildItem -Path $nodeRoot -Filter 'node-*' -Directory | Get-ChildItem | Move-Item -Destination $nodeRoot } else { Write-Verbose -Message ('tar -xJf "{0}" -C "{1}" --strip-components=1' -f $nodeZipFile,$nodeRoot) tar -xJf $nodeZipFile -C $nodeRoot '--strip-components=1' | Write-Verbose if( $LASTEXITCODE ) { Write-Error -Message ('Failed to extract Node.js {0} package "{1}" to "{2}".' -f $nodeVersionToInstall.version,$nodeZipFile,$nodeRoot) return } } $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $InstallRoot -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-Verbose ('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' | Write-Verbose if( $LASTEXITCODE ) { throw ('Failed to update to NPM {0}. Please see previous output for details.' -f $npmVersionToInstall) } } 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=$true)] [string] # The name of the module to install. $Name, [string] # The version of the module to install. $Version, [switch] # Node modules are being installed on a developer computer. $ForDeveloper, [Parameter(Mandatory)] [string] # The path to the build root. $BuildRootPath, [Switch] # Whether or not to install the module globally. $Global, [Switch] # Are we running in clean mode? $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-Verbose if( $LASTEXITCODE ) { return } $modulePath = Resolve-WhiskeyNodeModulePath -Name $Name -BuildRootPath $BuildRootPath -Global:$Global -ErrorAction Ignore if( -not $modulePath ) { Write-Error -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-WhiskeyNuGet { <# .SYNOPSIS Installs NuGet from its NuGet package. .DESCRIPTION The `Install-WhiskeyNuGet` function installs NuGet from its NuGet package. It returns the path to the `NuGet.exe` from the package. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] # Where to install NuGet. $DownloadRoot, [string] # The version to download. $Version ) Set-StrictMode -version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $versionParam = @{ } if( $Version ) { $versionParam['Version'] = $Version } $nuGetPath = Install-WhiskeyTool -NuGetPackageName 'NuGet.CommandLine' -DownloadRoot $DownloadRoot @versionParam return Join-Path -Path $nuGetPath -ChildPath 'tools\NuGet.exe' -Resolve } 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=$true)] [string] # The name of the module to install. $Name, [string] # The version of the module to install. $Version, [string] # Modules are saved into a PSModules directory. The "Path" parameter is the path where this PSModules directory should be, *not* the path to the PSModules directory itself, i.e. this is the path to the "PSModules" directory's parent directory. $Path = (Get-Location).ProviderPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Import-WhiskeyPowerShellModule -Name 'PackageManagement','PowerShellGet' Get-PackageProvider -Name 'NuGet' -ForceBootstrap | Out-Null $modulesRoot = Join-Path -Path $Path -ChildPath $powerShellModulesDirectoryName if( -not (Test-Path -Path $modulesRoot -PathType Container) ) { New-Item -Path $modulesRoot -ItemType 'Directory' -ErrorAction Stop | Out-Null } $expectedPath = Join-Path -Path $modulesRoot -ChildPath $Name if( (Test-Path -Path $expectedPath -PathType Container) -and (Get-ChildItem -Path $expectedPath -File -Filter ('{0}.psd1' -f $Name) -Recurse)) { Resolve-Path -Path $expectedPath | Select-Object -ExpandProperty 'ProviderPath' return } $module = Resolve-WhiskeyPowerShellModule -Name $Name -Version $Version if( -not $module ) { return } Write-Verbose -Message ('Saving PowerShell module {0} {1} to "{2}" from repository {3}.' -f $Name,$module.Version,$modulesRoot,$module.Repository) Save-Module -Name $Name -RequiredVersion $module.Version -Repository $module.Repository -Path $modulesRoot if( -not (Test-Path -Path $expectedPath -PathType Container) ) { Write-Error -Message ('Failed to download {0} {1} from {2} ({3}). Either the {0} module does not exist, or it does but version {1} does not exist. Browse the PowerShell Gallery at https://www.powershellgallery.com/' -f $Name,$Version,$module.Repository,$module.RepositorySourceLocation) } return $expectedPath } 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. 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 ahve 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( [Parameter(Mandatory=$true,ParameterSetName='Tool')] [Whiskey.RequiresToolAttribute] # The attribute that defines what tool is necessary. $ToolInfo, [Parameter(Mandatory=$true,ParameterSetName='Tool')] [string] # The directory where you want the tools installed. $InstallRoot, [Parameter(Mandatory=$true,ParameterSetName='Tool')] [hashtable] # The task parameters for the currently running task. $TaskParameter, [Parameter(ParameterSetName='Tool')] [Switch] # Running in clean mode, so don't install the tool if it isn't installed. $InCleanMode, [Parameter(Mandatory=$true,ParameterSetName='NuGet')] [string] # The name of the NuGet package to download. $NuGetPackageName, [Parameter(ParameterSetName='NuGet')] [string] # The version of the package to download. Must be a three part number, i.e. it must have a MAJOR, MINOR, and BUILD number. $Version, [Parameter(Mandatory=$true,ParameterSetName='NuGet')] [string] # 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`. $DownloadRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $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-Debug -Message ('Creating mutex "{0}".' -f $mutexName) $installLock = New-Object 'Threading.Mutex' $false,$mutexName #$DebugPreference = 'Continue' Write-Debug -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-Debug -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-Debug -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' ) { if( -not $IsWindows ) { Write-Error -Message ('Unable to install NuGet-based package {0} {1}: NuGet.exe is only supported on Windows.' -f $NuGetPackageName,$Version) -ErrorAction Stop return } $packagesRoot = Join-Path -Path $DownloadRoot -ChildPath 'packages' $version = Resolve-WhiskeyNuGetPackageVersion -NuGetPackageName $NuGetPackageName -Version $Version -NugetPath $whiskeyNuGetExePath if( -not $Version ) { return } $nuGetRootName = '{0}.{1}' -f $NuGetPackageName,$Version $nuGetRoot = Join-Path -Path $packagesRoot -ChildPath $nuGetRootName Set-Item -Path 'env:EnableNuGetPackageRestore' -Value 'true' if( -not (Test-Path -Path $nuGetRoot -PathType Container) ) { & $whiskeyNuGetExePath install $NuGetPackageName -version $Version -outputdirectory $packagesRoot | Write-CommandOutput -Description ('nuget.exe install') } return $nuGetRoot } elseif( $PSCmdlet.ParameterSetName -eq 'Tool' ) { $provider,$name = $ToolInfo.Name -split '::' if( -not $name ) { $name = $provider $provider = '' } $version = $TaskParameter[$ToolInfo.VersionParameterName] if( -not $version ) { $version = $ToolInfo.Version } switch( $provider ) { 'NodeModule' { $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $InstallRoot if( -not $nodePath ) { Write-Error -Message ('It looks like Node isn''t installed in your repository. Whiskey usually installs Node 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. If the .node directory exists, the Node package is most likely corrupt. Please delete it and re-run your build.') -ErrorAction stop return } $moduleRoot = Install-WhiskeyNodeModule -Name $name ` -BuildRootPath $InstallRoot ` -Version $version ` -Global ` -InCleanMode:$InCleanMode ` -ErrorAction Stop $TaskParameter[$ToolInfo.PathParameterName] = $moduleRoot } 'PowerShellModule' { $moduleRoot = Install-WhiskeyPowerShellModule -Name $name -Version $version -ErrorAction Stop $TaskParameter[$ToolInfo.PathParameterName] = $moduleRoot } default { switch( $name ) { 'Node' { $TaskParameter[$ToolInfo.PathParameterName] = Install-WhiskeyNode -InstallRoot $InstallRoot -Version $version -InCleanMode:$InCleanMode } 'DotNet' { $TaskParameter[$ToolInfo.PathParameterName] = Install-WhiskeyDotNetTool -InstallRoot $InstallRoot -WorkingDirectory (Get-Location).ProviderPath -Version $version -ErrorAction Stop } default { throw ('Unknown tool ''{0}''. The only supported tools are ''Node'' and ''DotNet''.' -f $name) } } } } } } finally { #$DebugPreference = 'Continue' $usedFor = (Get-Date) - $startedUsingAt Write-Debug -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-Debug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" released mutex "{2}" in {3}.' -f (Get-Date),$PID,$mutexName,$releasedDuration) #$DebugPreference = 'SilentlyContinue' } } 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=$true)] [Whiskey.Context] # The context for the build. Use `New-WhiskeyContext` to create context objects. $Context, [string[]] # 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. $PipelineName, [Parameter(Mandatory=$true,ParameterSetName='Clean')] [Switch] # 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. $Clean, [Parameter(Mandatory=$true,ParameterSetName='Initialize')] [Switch] # 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. $Initialize ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $Context.StartedAt = $script:buildStartedAt = Get-Date # 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/446 . $originalPSModulesPath = $env:PSModulePath $env:PSModulePath = '{0};{1};{2}' -f $whiskeyModulesRoot,(Join-Path -Path $Context.BuildRoot -ChildPath $powerShellModulesDirectoryName),$env:PSModulePath 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-Verbose -Message ('Publish? {0}' -f $Context.Publish) Write-Verbose -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-Verbose } Pop-Location $status = 'Failed' if( $succeeded ) { $status = 'Completed' } Set-WhiskeyBuildStatus -Context $Context -Status $status $env:PSModulePath = $originalPSModulesPath } } 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=$true)] [Whiskey.Context] # The `Whiskey.Context` object for the task running the command. $TaskContext, [Parameter(Mandatory=$true)] [string] # The path to the `dotnet` executable to run the SDK command with. $DotNetPath, [Parameter(Mandatory=$true)] [string] # The name of the .NET Core SDK command to run. $Name, [string[]] # A list of arguments to pass to the .NET Core SDK command. $ArgumentList, [string] # The path to a .NET Core solution or project file to pass to the .NET Core SDK command. $ProjectPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotNetExe = $DotNetPath | Resolve-Path -ErrorAction 'Ignore' if (-not $dotNetExe) { Write-Error -Message ('"{0}" does not exist.' -f $DotNetPath) return } $loggerArgs = & { '/filelogger9' $logFilePath = ('dotnet.{0}.log' -f $Name.ToLower()) if( $ProjectPath ) { $logFilePath = 'dotnet.{0}.{1}.log' -f $Name.ToLower(),($ProjectPath | Split-Path -Leaf) } $logFilePath = Join-Path -Path $TaskContext.OutputDirectory.FullName -ChildPath $logFilePath ('/flp9:LogFile={0};Verbosity=d' -f $logFilePath) } $commandInfoArgList = & { $Name $ArgumentList $loggerArgs $ProjectPath } Write-WhiskeyCommand -Context $TaskContext -Path $dotNetExe -ArgumentList $commandInfoArgList Invoke-Command -ScriptBlock { param( $DotNetExe, $Command, $DotNetArgs, $LoggerArgs, $Project ) & $DotNetExe $Command $DotNetArgs $LoggerArgs $Project } -ArgumentList $dotNetExe,$Name,$ArgumentList,$loggerArgs,$ProjectPath if ($LASTEXITCODE -ne 0) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('"{0}" failed with exit code {1}' -f $DotNetExe,$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', '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=$true)] [string] # The NPM command to execute, e.g. `install`, `prune`, `run-script`, etc. $Name, [string[]] # An array of arguments to be given to the NPM command being executed. $ArgumentList, [Parameter(Mandatory=$true)] [string] $BuildRootPath, [switch] # NPM commands are being run on a developer computer. $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-Error -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' -and $ErrorActionPreference -ne 'Ignore' ) { $ErrorActionPreference = 'Continue' } try { Write-Verbose ('{0} {1} {2} {3}' -f $nodePath,$npmPath,$commandName,($commandArgs -join ' ')) & $nodePath $npmPath $commandName $commandArgs } finally { Write-Verbose -Message ($LASTEXITCODE) $ErrorActionPreference = $originalEap } } if( $LASTEXITCODE -ne 0 ) { Write-Error -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=$true)] [string] $Path, [Parameter(Mandatory=$true)] [string] $Uri, [Parameter(Mandatory=$true)] [string] $ApiKey, [Parameter(Mandatory=$true)] [string] $NuGetPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState & $NuGetPath push $Path -Source $Uri -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=$true)] [object] # The current build context. Use the `New-WhiskeyContext` function to create a context object. $Context, [Parameter(Mandatory=$true)] [string] # 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. $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-Warning -Message ('It looks like pipeline ''{0}'' doesn''t have any tasks.' -f $Context.ConfigurationPath) $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=$true)] [string] $Source, [Parameter(Mandatory=$true)] [string] $Destination, [string[]] $WhiteList, [string[]] $Exclude, [string] $LogPath ) 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 $logParam = '' if ($LogPath) { $logParam = '/LOG:{0}' -f $LogPath } $excludeParam = $Exclude | ForEach-Object { '/XF' ; $_ ; '/XD' ; $_ } robocopy $Source $Destination '/PURGE' '/S' '/NP' '/R:0' '/NDL' '/NFL' '/NS' ('/MT:{0}' -f $numRobocopyThreads) $WhiteList $excludeParam $logParam } function Invoke-WhiskeyTask { <# .SYNOPSIS Runs a Whiskey task. .DESCRIPTION The `Invoke-WhiskeyTask` function runs a Whiskey task. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] # The context this task is operating in. Use `New-WhiskeyContext` to create context objects. $TaskContext, [Parameter(Mandatory=$true)] [string] # The name of the task. $Name, [Parameter(Mandatory=$true)] [hashtable] # The parameters/configuration to use to run the task. $Parameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState function Invoke-Event { param( $EventName, $Property ) 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-Warning -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 } $TaskContext.TaskName = $Name if( $task.Obsolete ) { $message = 'The "{0}" task is obsolete and shouldn''t be used.' -f $task.Name if( $task.ObsoleteMessage ) { $message = $task.ObsoleteMessage } Write-WhiskeyWarning -Context $TaskContext -Message $message } if( -not $task.Platform.HasFlag($CurrentPlatform) ) { Write-Error -Message ('Unable to run task "{0}": it is only supported on the {1} platform(s) and we''re currently running on {2}.' -f $task.Name,$task.Platform,$CurrentPlatform) -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', 'WorkingDirectory', 'IfExists', 'UnlessExists', 'OnlyOnPlatform', 'ExceptOnPlatform' ) ) { if ($taskProperties.ContainsKey($commonPropertyName)) { $commonProperties[$commonPropertyName] = $taskProperties[$commonPropertyName] $taskProperties.Remove($commonPropertyName) } } $workingDirectory = $TaskContext.BuildRoot if( $Parameter['WorkingDirectory'] ) { $workingDirectory = $Parameter['WorkingDirectory'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'WorkingDirectory' } $taskTempDirectory = '' $requiredTools = Get-RequiredTool -CommandName $task.CommandName $startedAt = Get-Date $result = 'FAILED' $currentDirectory = [IO.Directory]::GetCurrentDirectory() Push-Location -Path $workingDirectory [IO.Directory]::SetCurrentDirectory($workingDirectory) 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 } } foreach( $requiredTool in $requiredTools ) { Install-WhiskeyTool -ToolInfo $requiredTool ` -InstallRoot $TaskContext.BuildRoot ` -TaskParameter $taskProperties ` -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 } Invoke-Event -EventName 'BeforeTask' -Property $taskProperties Invoke-Event -EventName ('Before{0}Task' -f $Name) -Property $taskProperties Write-WhiskeyVerbose -Context $TaskContext -Message '' $startedAt = Get-Date $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 } $parameter = Get-TaskParameter -Name $task.CommandName -TaskProperty $taskProperties -Context $TaskContext & $task.CommandName @parameter $result = 'COMPLETED' } finally { # 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 -InstallRoot $TaskContext.BuildRoot -Name $requiredTool.Name } } if( $taskTempDirectory -and (Test-Path -Path $taskTempDirectory -PathType Container) ) { Remove-Item -Path $taskTempDirectory -Recurse -Force -ErrorAction Ignore } $endedAt = Get-Date $duration = $endedAt - $startedAt Write-WhiskeyVerbose -Context $TaskContext -Message ('{0} in {1}' -f $result,$duration) Write-WhiskeyVerbose -Context $TaskContext -Message '' [IO.Directory]::SetCurrentDirectory($currentDirectory) Pop-Location } Invoke-Event -EventName 'AfterTask' -Property $taskProperties Invoke-Event -EventName ('After{0}Task' -f $Name) -Property $taskProperties } 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( [Parameter(Mandatory=$true)] [string] # The environment you're building in. $Environment, [Parameter(Mandatory=$true)] [string] # The path to the `whiskey.yml` file that defines build settings and tasks. $ConfigurationPath, [string] # The place where downloaded tools should be cached. The default is the build root. $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) } $buildPipelineName = 'Build' if( $config.ContainsKey('BuildTasks') ) { $buildPipelineName = 'BuildTasks' Write-Warning ('{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-Warning ('{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 $prereleaseInfo = '' if( $byBuildServer ) { $branch = $buildMetadata.ScmBranch if( $config.ContainsKey( 'PublishOn' ) ) { Write-Verbose -Message ('PublishOn') foreach( $publishWildcard in $config['PublishOn'] ) { $publish = $branch -like $publishWildcard if( $publish ) { Write-Verbose -Message (' {0} -like {1}' -f $branch,$publishWildcard) break } else { Write-Verbose -Message (' {0} -notlike {1}' -f $branch,$publishWildcard) } } } } $versionTaskExists = $config[$buildPipelineName] | Where-Object { $_ -and ($_ | Get-Member -Name 'Keys') } | Where-Object { $_.Keys | Where-Object { $_ -eq 'Version' } } if( -not $versionTaskExists -and ($config.ContainsKey('PrereleaseMap') -or $config.ContainsKey('Version') -or $config.ContainsKey('VersionFrom')) ) { Write-Warning ('{0}: The ''PrereleaseMap'', ''Version'', and ''VersionFrom'' properties are obsolete and will be removed in Whiskey 1.0. They were replaced with the ''Version'' task. Add a ''Version'' task as the first task in your build pipeline. If your current whiskey.yml file looks like this: Version: 1.2.3 PrereleaseMap: - "alpha/*": alpha - "release/*": rc add a Version task to your build pipeline that looks like this: Build: - Version: Version: 1.2.3 Prerelease: - "alpha/*": alpha.$(WHISKEY_BUILD_NUMBER) - "release/*": rc.$(WHISKEY_BUILD_NUMBER) You must add the ".$(WHISKEY_BUILD_NUMBER)" string to each prerelease version. Whiskey no longer automatically adds a prerelease version number for you. If you use the "VersionFrom" property, your whiskey.yml file looks something like this: VersionFrom: Whiskey\Whiskey.psd1 Update it to look like this: Build: - Version: Path: Whiskey\Whiskey.psd1 Whiskey also no longer automatically adds build metadata to your version number. To preserve Whiskey''s old default build metadata, add a "Build" property to your new "Version" task that looks like this: Build: - Version: Version: 1.2.3 Build: $(WHISKEY_SCM_BRANCH).$(WHISKEY_SCM_COMMIT_ID.Substring(0,7)) ' -f $ConfigurationPath) $versionTask = $null $versionTask = @{ Version = ('{0:yyyy.Mdd}.$(WHISKEY_BUILD_NUMBER)' -f (Get-Date)) Build = '$(WHISKEY_SCM_BRANCH).$(WHISKEY_SCM_COMMIT_ID)' } if( $config['Version'] ) { $versionTask['Version'] = $config['Version'] } elseif( $config['VersionFrom'] ) { $versionTask.Remove('Version') $versionTask['Path'] = $config['VersionFrom'] } if( $config['PrereleaseMap'] ) { $versionTask['Prerelease'] = $config['PrereleaseMap'] | ForEach-Object { if( -not ($_ | Get-Member 'Keys') ) { return $_ } $newMap = @{ } foreach( $key in $_.Keys ) { $value = $_[$key] $newMap[$key] = '{0}.$(WHISKEY_BUILD_NUMBER)' -f $value } $newMap } } if( $versionTask ) { if( -not $config[$buildPipelineName] ) { $config[$buildPipelineName] = @() } $config[$buildPipelineName] = & { @{ Version = $versionTask } $config[$buildPipelineName] } } } $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-Error -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 } if( $versionTaskExists ) { $context.Version = New-WhiskeyVersionObject -SemVer '0.0.0' } else { Write-Warning ('Whiskey''s default, date-based version number is OBSOLETE. Beginning with Whiskey 1.0, the default Whiskey version number will be 0.0.0. Use the Version task to set your own custom version. For example, this Version task preserves the existing behavior: Build - Version: Version: $(WHISKEY_BUILD_STARTED_AT.ToString(''yyyy.Mdd'')).$(WHISKEY_BUILD_NUMBER) Build: $(WHISKEY_SCM_BRANCH).$(WHISKEY_SCM_COMMIT_ID) ') $rawVersion = '{0:yyyy.Mdd}.{1}' -f (Get-Date),$context.BuildMetadata.BuildNumber if( $context.ByBuildServer ) { $branch = $buildMetadata.ScmBranch $branch = $branch -replace '[^A-Za-z0-9-]','-' $commitID = $buildMetadata.ScmCommitID.Substring(0,7) $buildInfo = '{0}.{1}.{2}' -f $buildMetadata.BuildNumber,$branch,$commitID $rawVersion = '{0}+{1}' -f $rawVersion,$buildInfo } $context.Version = New-WhiskeyVersionObject -SemVer $rawVersion } 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=$true)] [string] # The path to the Pester test resut. $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-Verbose -Message ('Uploading Pester test result file ''{0}'' to AppVeyor at ''{1}''.' -f $resultPath,$uploadUri) $webClient.UploadFile($uploadUri, $resultPath) } } 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=$true)] [object] $TaskContext, [Parameter(Mandatory=$true)] [string] $TaskName, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) } To stop a build while handling an event, call the `Stop-WhiskeyTask` function. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] # The name of the command to run during the event. $CommandName, [Parameter(Mandatory=$true)] [string] [ValidateSet('BeforeTask','AfterTask')] # When the command should be run; what events does it respond to? $Event, [string] # Only fire the event for a specific task. $TaskName ) Set-StrictMode -Version 'Latest' $eventName = $Event if( $TaskName ) { $eventType = $Event -replace 'Task$','' $eventName = '{0}{1}Task' -f $eventType,$TaskName } if( -not $events[$eventName] ) { $events[$eventName] = New-Object -TypeName 'Collections.Generic.List[string]' } $events[$eventName].Add( $CommandName ) } 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 if( (Test-Path -Path $Path -PathType Leaf) ) { Remove-Item -Path $Path -Force } elseif( (Test-Path -Path $Path -PathType Container) ) { if( $IsWindows ) { $logPath = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ('whiskey.robocopy.{0}.log' -f ([IO.Path]::GetRandomFileName())) $emptyDir = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ([IO.Path]::GetRandomFileName()) $deleteLog = $true New-Item -Path $emptyDir -ItemType 'Directory' | Out-Null try { Invoke-WhiskeyRobocopy -Source $emptyDir -Destination $Path -LogPath $logPath | Out-Null if( $LASTEXITCODE -ge 8 ) { $deleteLog = $false Write-Error -Message ('Failed to remove directory "{0}". See "{1}" for more information.' -f $Path,$logPath) return } Remove-Item -Path $Path -Recurse -Force } finally { if( $deleteLog ) { Remove-Item -Path $logPath -ErrorAction Ignore -Force } Remove-Item -Path $emptyDir -Recurse -Force } } else { Remove-Item -Path $Path -Recurse -Force } } } 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. .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. #> [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 ) Set-StrictMode -version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if ($Version) { $releasesIndexUri = 'https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json' $releasesIndex = Invoke-RestMethod -Uri $releasesIndexUri -ErrorAction Stop | Select-Object -ExpandProperty 'releases-index' | Where-Object { [Version]::TryParse($_.'channel-version', [ref]$null) } | ForEach-Object { $_.'channel-version' = [Version]$_.'channel-version' $_ } | Sort-Object -Property 'channel-version' -Descending $Version -match '^\d+\.(?:\d+|\*)|^\*' | Out-Null $matcher = $Matches[0] $release = $releasesIndex | Where-Object { $_.'channel-version' -like $matcher } | Select-Object -First 1 if (-not $release) { Write-Error -Message ('.NET Core release matching "{0}" could not be found in "{1}"' -f $matcher, $releasesIndexUri) return } $releasesJsonUri = $release | Select-Object -ExpandProperty 'releases.json' Write-Verbose -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' } $resolvedVersion = $sdkVersions | ForEach-Object { $_ -as [Version] } | Where-Object { $_ -like $Version } | Sort-Object -Descending | Select-Object -First 1 if (-not $resolvedVersion) { Write-Error -Message ('A released version of the .NET Core SDK matching "{0}" could not be found in "{1}"' -f $Version, $releasesJsonUri) return } Write-Verbose -Message ('[{0}] SDK version "{1}" resolved to "{2}"' -f $MyInvocation.MyCommand,$Version,$resolvedVersion) } else { $latestLTSVersionUri = 'https://dotnetcli.blob.core.windows.net/dotnet/Sdk/LTS/latest.version' Write-Verbose -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-Error -Message ('Could not retrieve the latest LTS version of the .NET Core SDK. "{0}" returned:{1}{2}' -f $latestLTSVersionUri,[Environment]::NewLine,$latestLTSVersion) return } Write-Verbose -Message ('[{0}] Latest LTS version resolved as: "{1}"' -f $MyInvocation.MyCommand,$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)] [string] # The name of the Node module whose path to get. $Name, [Parameter(Mandatory,ParameterSetName='FromBuildRoot')] [string] # 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. $BuildRootPath, [Parameter(ParameterSetName='FromBuildRoot')] [Switch] # 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. $Global, [Parameter(Mandatory,ParameterSetName='FromNodeRoot')] [string] # The path to the root of a Node package, as downloaded and expanded from the Node.js project. $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-Error -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')] [string] # The path to the build root. This will return the path to Node where Whiskey installs a local copy. $BuildRootPath, [Parameter(Mandatory,ParameterSetName='FromNodeRoot')] [string] # The path to the root of an Node package, as downloaded and expanded from the Node.js download page. $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-Debug -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-Error -Message ('Node executable doesn''t exist in "{0}".' -f $NodeRootPath) -ErrorAction $ErrorActionPreference return } Write-Debug -Message ('Found Node executable at "{0}".' -f $nodePath) return $nodePath } function Resolve-WhiskeyNuGetPackageVersion { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] # The name of the NuGet package to download. $NuGetPackageName, [string] # The version of the package to download. Must be a three part number, i.e. it must have a MAJOR, MINOR, and BUILD number. $Version, [string] $NugetPath = ($whiskeyNuGetExePath) ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $Version ) { Set-Item -Path 'env:EnableNuGetPackageRestore' -Value 'true' $NuGetPackage = Invoke-Command -NoNewScope -ScriptBlock { & $NugetPath list ('packageid:{0}' -f $NuGetPackageName) } $Version = $NuGetPackage | Where-Object { $_ -match $NuGetPackageName } | Where-Object { $_ -match ' (\d+\.\d+\.\d+.*)' } | ForEach-Object { $Matches[1] } | Select-Object -First 1 if( -not $Version ) { Write-Error ("Unable to find latest version of package '{0}'." -f $NuGetPackageName) return } } elseif( [Management.Automation.WildcardPattern]::ContainsWildcardCharacters($version) ) { Write-Error "Wildcards are not allowed for NuGet packages yet because of a bug in the nuget.org search API (https://github.com/NuGet/NuGetGallery/issues/3274)." return } return $Version } function Resolve-WhiskeyPowerShellModule { <# .SYNOPSIS Searches for a PowerShell module using PowerShellGet to ensure it exists and returns the resulting object from PowerShellGet. .DESCRIPTION The `Resolve-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 Resolve-WhiskeyPowerShellModule -Name 'Pester' Demonstrates getting the module info on the latest version of the Pester module. .EXAMPLE Resolve-WhiskeyPowerShellModule -Name 'Pester' -Version '4.*' Demonstrates getting the module info on the latest '4.X' version of the Pester module. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] # The name of the PowerShell module. $Name, [string] # 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. $Version ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Import-WhiskeyPowerShellModule -Name 'PackageManagement', 'PowerShellGet' Get-PackageProvider -Name 'NuGet' -ForceBootstrap | Out-Null if( $Version ) { $atVersionString = ' at version {0}' -f $Version if( -not [Management.Automation.WildcardPattern]::ContainsWildcardCharacters($version) ) { $tempVersion = [Version]$Version if( $TempVersion -and ($TempVersion.Build -lt 0) ) { $Version = [version]('{0}.{1}.0' -f $TempVersion.Major, $TempVersion.Minor) } } $module = Find-Module -Name $Name -AllVersions | Where-Object { $_.Version.ToString() -like $Version } | Sort-Object -Property 'Version' -Descending } else { $atVersionString = '' $module = Find-Module -Name $Name -ErrorAction Ignore } if( -not $module ) { Write-Error -Message ('Failed to find module {0}{1} module on the PowerShell Gallery. You can browse the PowerShell Gallery at https://www.powershellgallery.com/' -f $Name, $atVersionString) return } return $module | Select-Object -First 1 } function Resolve-WhiskeyTaskPath { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [object] # An object that holds context about the current build and executing task. $TaskContext, [Parameter(Mandatory=$true,ValueFromPipeline=$true)] [string] $Path, [Parameter(Mandatory=$true)] [string] $PropertyName, [string] # 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. $ParentPath, [Switch] # 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. $Force, [string] [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. $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-Debug } 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-Debug -Message $message Resolve-Path -Path $Path | Select-Object -ExpandProperty 'ProviderPath' | ForEach-Object { Write-Debug -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 `about_Whiskey_Variables` help topic 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()] param( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] [AllowNull()] [object] # 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. $InputObject, [Parameter(Mandatory=$true)] [Whiskey.Context] # The context of the current build. Necessary to lookup any variables. $Context ) begin { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $version = $Context.Version $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_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( $InputObject -eq $null ) { 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-Error -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-Error -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-Error -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-Error -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=$true)] [object] $Context, [Parameter(Mandatory=$true)] [ValidateSet('Started','Completed','Failed')] # The build status. Should be one of `Started`, `Completed`, or `Failed`. $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.*' Import-WhiskeyPowerShellModule -Name 'BitbucketServerAutomation' $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-WhiskeyDotNetGlobalJson { <# .SYNOPSIS Sets values within a .NET Core global.json file. .DESCRIPTION The `Set-WhiskeyDotNetGlobalJson` function sets values within a .NET Core `global.json` file. If the `global.json` file does not exist in the given `Directory` then it will be created. If the `global.json` file already exists, then the function will only update the desired values and leave the rest of the content as-is. .EXAMPLE Set-WhiskeyDotNetGlobalJson -Directory 'C:\Build\app' -SdkVersion '2.1.4' Demonstrates setting the `sdk.version` property in global.json to '2.1.4'. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] # The directory where the `global.json` will be created/modified. $Directory, [Parameter(Mandatory=$true)] [string] # The version of the SDK to set within the `global.json` file. $SdkVersion ) Set-StrictMode -version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-Verbose -Message ('[{0}] Setting global.json properties: ''sdk.version'' => ''{1}''' -f $MyInvocation.MyCommand,$SdkVersion) if (-not (Test-Path -Path $Directory -PathType Container)) { Write-Error -Message ('The directory ''{0}'' does not exist.' -f $Directory) return } $globalJsonPath = Join-Path -Path $Directory -ChildPath 'global.json' Write-Verbose -Message ('[{0}] Looking for global.json at ''{1}''' -f $MyInvocation.MyCommand,$globalJsonPath) if (Test-Path -Path $globalJsonPath -PathType Leaf) { Write-Verbose -Message ('[{0}] Found existing global.json' -f $MyInvocation.MyCommand) Write-Verbose -Message ('[{0}] Updating ''{1}''' -f $MyInvocation.MyCommand,$globalJsonPath) try { $globalJson = Get-Content -Path $globalJsonPath -Raw | ConvertFrom-Json } catch { Write-Error -Message ('global.json file ''{0}'' contains invalid JSON.' -f $globalJsonPath) return } if (-not ($globalJson | Get-Member -Name 'sdk')) { $globalJson | Add-Member -MemberType NoteProperty -Name 'sdk' -Value ([pscustomobject]@{ }) } if (-not ($globalJson.sdk | Get-Member -Name 'version')) { $globalJson.sdk | Add-Member -MemberType NoteProperty -Name 'version' -Value ([pscustomobject]@{ }) } $globalJson.sdk.version = $SdkVersion } else { Write-Verbose -Message ('[{0}] global.json does not exist at ''{1}''' -f $MyInvocation.MyCommand,$globalJsonPath) Write-Verbose -Message ('[{0}] Creating ''{1}''' -f $MyInvocation.MyCommand,$globalJsonPath) $globalJson = @{ 'sdk' = @{ 'version' = $SdkVersion } } } $globalJson | ConvertTo-Json -Depth 100 | Set-Content -Path $globalJsonPath -Force Write-Verbose -Message ('[{0}] global.json update finished.' -f $MyInvocation.MyCommand) } 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=$true)] [Whiskey.Context] # The context of the build whose MSBuild configuration you want to set. Use `New-WhiskeyContext` to create a context. $Context, [Parameter(Mandatory=$true)] [string] # The configuration to use. $Value ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $Context.MSBuildConfiguration = $Value } function Stop-Whiskey { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [object] # An object $Context, [Parameter(Mandatory=$true)] [string] $Message ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState throw '{0}: {1}' -f $Context.ConfigurationPath,$Message } function Stop-WhiskeyTask { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [object] # An object $TaskContext, [Parameter(Mandatory=$true)] [string] $Message, [string] $PropertyName, [string] $PropertyDescription ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not ($PropertyDescription) ) { $PropertyDescription = 'Build[{0}]: Task "{1}"' -f $TaskContext.TaskIndex,$TaskContext.TaskName } if( $PropertyName ) { $PropertyName = ': Property "{0}"' -f $PropertyName } if( $ErrorActionPreference -ne 'Ignore' ) { throw '{0}: {1}{2}: {3}' -f $TaskContext.ConfigurationPath,$PropertyDescription,$PropertyName,$Message } } 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=$true)] [Whiskey.Context] # The context for the build. $Context, [Parameter(Mandatory=$true)] [hashtable] # The common task properties defined for the current task. $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']) -Verbose return $true } Write-WhiskeyVerbose -Context $Context -Message ('IfExists {0} exists' -f $Properties['IfExists']) -Verbose } if( $Properties['UnlessExists'] ) { $exists = Test-Path -Path $Properties['UnlessExists'] if( $exists ) { Write-WhiskeyVerbose -Context $Context -Message ('UnlessExists {0} exists' -f $Properties['UnlessExists']) -Verbose return $true } Write-WhiskeyVerbose -Context $Context -Message ('UnlessExists {0} not exists' -f $Properties['UnlessExists']) -Verbose } 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-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=$true)] [string] # The name of the module to uninstall. $Name, [Parameter(Mandatory=$true)] [string] # The path to the build root directory. $BuildRootPath, [switch] # Node modules are being uninstalled on a developer computer. $ForDeveloper, [switch] # Remove the module manually if NPM fails to uninstall it $Force, [Switch] # Uninstall the module from the global cache. $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-Error -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-Error -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=$true)] [string] # The name of the module to uninstall. $Name, [string] $Version = '*.*.*', # Modules are saved into a PSModules directory. The "Path" parameter is the path where this PSModules directory should be, *not* the path to the PSModules directory itself, i.e. this is the path to the "PSModules" directory's parent directory. $Path = (Get-Location).ProviderPath ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Get-Module -Name $Name | Remove-Module -Force $modulesRoot = Join-Path -Path $Path -ChildPath $powerShellModulesDirectoryName # Remove modules saved by either PowerShell4 or PowerShell5 $moduleRoots = @( ('{0}\{1}' -f $Name, $Version), ('{0}' -f $Name) ) 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' -InstallRoot $TaskContext.BuildRoot Demonstrates how to uninstall Node from the `.node` directory in your build root. .EXAMPLE Uninstall-WhiskeyTool -Name 'NodeModule::rimraf' -InstallRoot $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' -InstallRoot $TaskContext.BuildRoot Demonstrates how to uninstall the .NET Core SDK from the `.dotnet` directory in your build root. #> [CmdletBinding()] param( [Parameter(Mandatory=$true,ParameterSetName='Tool')] [string] # The name of the tool to uninstall. Currently only Node is supported. $Name, [Parameter(Mandatory=$true,ParameterSetName='Tool')] [string] # The directory where the tool should be uninstalled from. $InstallRoot, [Parameter(Mandatory=$true,ParameterSetName='NuGet')] [string] # The name of the NuGet package to uninstall. $NuGetPackageName, [String] # The version of the package to uninstall. Must be a three part number, i.e. it must have a MAJOR, MINOR, and BUILD number. $Version, [Parameter(Mandatory=$true,ParameterSetName='NuGet')] [string] # The build root where the build is currently running. Tools are installed here. $BuildRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $PSCmdlet.ParameterSetName -eq 'NuGet' ) { $Version = Resolve-WhiskeyNuGetPackageVersion -NuGetPackageName $NuGetPackageName -Version $Version -NugetPath $whiskeyNuGetExePath if( -not $Version ) { return } $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 } } elseif( $PSCmdlet.ParameterSetName -eq 'Tool' ) { $provider,$Name = $Name -split '::' if( -not $Name ) { $Name = $provider $provider = '' } 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. } 'PowerShellModule' { Uninstall-WhiskeyPowerShellModule -Name $Name } default { switch( $Name ) { 'Node' { $dirToRemove = Join-Path -Path $InstallRoot -ChildPath '.node' Remove-WhiskeyFileSystemItem -Path $dirToRemove } 'DotNet' { $dotnetToolRoot = Join-Path -Path $InstallRoot -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=$true)] [string] # The name of the command to run during the event. $CommandName, [Parameter(Mandatory=$true)] [string] [ValidateSet('BeforeTask','AfterTask')] # When the command should be run; what events does it respond to? $Event, [string] # The specific task whose events to unregister. $TaskName ) Set-StrictMode -Version 'Latest' $eventName = $Event if( $TaskName ) { $eventType = $Event -replace 'Task$','' $eventName = '{0}{1}Task' -f $eventType,$TaskName } if( -not $events[$eventName] ) { return } if( -not $Events[$eventName].Contains( $CommandName ) ) { return } $events[$eventName].Remove( $CommandName ) } # 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 = $true)] #[Management.Automation.PSScriptCmdlet] # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` attribute. $Cmdlet, [Parameter(Mandatory = $true)] [Management.Automation.SessionState] # 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. $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) } } } Set-StrictMode -version 'latest' function Write-CommandOutput { param( [Parameter(ValueFromPipeline=$true)] [string] $InputObject, [Parameter(Mandatory=$true)] [string] $Description ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $InputObject -match '^WARNING\b' ) { $InputObject | Write-Warning } elseif( $InputObject -match '^ERROR\b' ) { $InputObject | Write-Error } else { $InputObject | ForEach-Object { Write-Verbose -Message ('[{0}] {1}' -f $Description,$InputObject) } } } } function Write-WhiskeyCommand { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $Context, [string] $Path, [string[]] $ArgumentList ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $logArgumentList = Invoke-Command { if( $Path -match '\ ' ) { '&' } $Path $ArgumentList } | ForEach-Object { if( $_ -match '\ ' ) { '"{0}"' -f $_.Trim('"',"'") } else { $_ } } Write-WhiskeyInfo -Context $TaskContext -Message ($logArgumentList -join ' ') Write-WhiskeyVerbose -Context $TaskContext -Message $path $argumentPrefix = ' ' * ($path.Length + 2) foreach( $argument in $ArgumentList ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('{0}{1}' -f $argumentPrefix,$argument) } } function Write-WhiskeyInfo { <# .SYNOPSIS Writes `Info` level messages. .DESCRIPTION The `Write-WhiskeyInfo` function writes `Info` level messages during a build with a prefix that identifies the current pipeline and task being executed. Pass the `Whiskey.Context` object to the `Context` parameter and the message to write to the `Message` parameter. Optionally, you may specify a custom indentation level for the message with the `Indent` parameter. The default message indentation is 1 space. This function uses PowerShell's [Write-Information](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-information) cmdlet to write the message. If `Write-Information` is not supported, e.g. below PowerShell 5.0, the function falls back to using [Write-Output](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-output). .EXAMPLE Write-WhiskeyInfo -Context $context -Message 'An info message' Demonstrates how write an `Info` message. .EXAMPLE Write-WhiskeyInfo -Context $context -Message 'An info message' -Indent 2 Demonstrates how write an `Info` message with a custom indent. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] # The current context. [Whiskey.Context]$Context, [Parameter(Mandatory=$true,ValueFromPipeline=$true)] [AllowEmptyString()] [AllowNull()] # The message to write. [string]$Message, [int]$Indent = 0 ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $Message = Format-WhiskeyMessage @PSBoundParameters if( $supportsWriteInformation ) { Write-Information -MessageData $Message -InformationAction Continue } else { Write-Output -InputObject $Message } } } function Write-WhiskeyTiming { [CmdletBinding()] param( $Message ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $now = Get-Date Write-Debug -Message ('[{0}] [{1}] {2}' -f $now,($now - $buildStartedAt),$Message) } function Write-WhiskeyVerbose { <# .SYNOPSIS Writes `Verbose` level messages. .DESCRIPTION The `Write-WhiskeyVerbose` function writes `Verbose` level messages during a build with a prefix that identifies the current pipeline and task being executed. Pass the `Whiskey.Context` object to the `Context` parameter and the message to write to the `Message` parameter. Optionally, you may specify a custom indentation level for the message with the `Indent` parameter. The default message indentation is 1 space. This function uses PowerShell's [Write-Verbose](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-verbose) cmdlet to write the message. .EXAMPLE Write-WhiskeyVerbose -Context $context -Message 'A verbose message' Demonstrates how write a `Verbose` message. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] # The current context. [Whiskey.Context]$Context, [Parameter(Mandatory=$true,ValueFromPipeline=$true)] [AllowEmptyString()] [AllowNull()] # The message to write. [string]$Message, [int]$Indent = 0 ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Write-Verbose -Message (Format-WhiskeyMessage @PSBoundParameters) } } function Write-WhiskeyWarning { <# .SYNOPSIS Writes `Warning` level messages. .DESCRIPTION The `Write-WhiskeyWarning` function writes `Warning` level messages during a build with a prefix that identifies the current build configuration and task being executed. Pass the `Whiskey.Context` object to the `Context` parameter and the message to write to the `Message` parameter. Optionally, you may specify a custom indentation level for the message with the `Indent` parameter. The default message indentation is 1 space. This function uses PowerShell's [Write-Warning](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-warning) cmdlet to write the message. .EXAMPLE Write-WhiskeyWarning -Context $context -Message 'A warning message' Demonstrates how write a `Warning` message. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] # The current context. [Whiskey.Context]$Context, [Parameter(Mandatory=$true,ValueFromPipeline=$true)] # The message to write. [string]$Message, [int]$Indent = 0 ) Set-StrictMode -Version 'Latest' Write-Warning -Message (Format-WhiskeyMessage @PSBoundParameters) } function Copy-WhiskeyFile { [Whiskey.Task("CopyFile")] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $pathErrorMessage = @' 'Path' property is missing. Please set it to the list of files to copy, e.g. Build: - CopyFile: Path: myfile.txt Destination: \\computer\share '@ $destDirErrorMessage = @' 'DestinationDirectory' property is missing. Please set it to the list of target locations to copy to, e.g. Build: - CopyFile: Path: myfile.txt DestinationDirectory: \\computer\share '@ if(!$TaskParameter.ContainsKey('Path')) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ($pathErrorMessage) return } $sourceFiles = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' if(!$sourceFiles) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ($pathErrorMessage) return } if(!$TaskParameter.ContainsKey('DestinationDirectory')) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ($destDirErrorMessage) return } if(!$TaskParameter['DestinationDirectory']) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ($destDirErrorMessage) return } foreach($sourceFile in $sourceFiles) { if((Test-Path -Path $sourceFile -PathType Container)) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Path "{0}" is directory. The CopyFile task only copies files. Please remove this path from your "Path" property.' -f $sourceFile) return } } $idx = 0 $destinations = $TaskParameter['DestinationDirectory'] | ForEach-Object { $path = $_ if( -not [IO.Path]::IsPathRooted($path) ) { $path = Join-Path -Path $TaskContext.BuildRoot -ChildPath $path } if( [Management.Automation.WildcardPattern]::ContainsWildcardCharacters($path) ) { $path = Resolve-Path -Path $path -ErrorAction Ignore | Select-Object -ExpandProperty 'ProviderPath' if( -not $path ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('DestinationDirectory[{0}]: Wildcard pattern "{1}" doesn''t point to an existing directory.' -f $idx, $_) return } $path } else { $path } $idx++ } foreach ($destDir in $destinations) { if(!(Test-Path -Path $destDir -PathType Container)) { $null = New-Item -Path $destDir -ItemType 'Directory' -Force } if(!(Test-Path -Path $destDir -PathType Container)) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failed to create destination directory "{0}". Make sure the current user, "{1}\{2}" has access to create directories in "{0}". If it is a file share, check that the share exists and the share"s permissions.' -f $destDir, [Environment]::UserDomainName, [Environment]::UserName) return } } foreach( $destDir in $destinations ) { foreach($sourceFile in $sourceFiles) { Write-WhiskeyVerbose -Context $TaskContext ('{0} -> {1}' -f $sourceFile,$destDir) Copy-Item -Path $sourceFile -Destination $destDir } } } function Remove-WhiskeyItem { [Whiskey.TaskAttribute('Delete', SupportsClean=$true)] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState foreach( $path in $TaskParameter['Path'] ) { $path = $path | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' -ErrorAction Ignore if( -not $path ) { continue } foreach( $pathItem in $path ) { Remove-WhiskeyFileSystemItem -Path $pathitem -ErrorAction Stop } } } function Invoke-WhiskeyDotNet { [CmdletBinding()] [Whiskey.Task("DotNet")] [Whiskey.RequiresTool('DotNet','DotNetPath',VersionParameterName='SdkVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) 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 DotNetPath = $dotnetExe Name = $command ArgumentList = $TaskParameter['Argument'] } 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' 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','DotNetPath',VersionParameterName='SdkVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotnetExe = $TaskParameter['DotNetPath'] $projectPaths = '' if ($TaskParameter['Path']) { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -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','DotNetPath',VersionParameterName='SdkVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotnetExe = $TaskParameter['DotNetPath'] $projectPaths = '' if ($TaskParameter['Path']) { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -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','DotNetPath',VersionParameterName='SdkVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotnetExe = $TaskParameter['DotNetPath'] $projectPaths = '' if ($TaskParameter['Path']) { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -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','DotNetPath',VersionParameterName='SdkVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $dotnetExe = $TaskParameter['DotNetPath'] $projectPaths = '' if ($TaskParameter['Path']) { $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -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=$true,SupportsInitialize=$true)] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [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 Get-WhiskeyPowerShellModule { <# .SYNOPSIS Downloads a PowerShell module. .DESCRIPTION The `GetPowerShellModule` task downloads a PowerShell module and saves it into a `Modules` directory in the build root. Use the `Name` property to specify the name of the module to download. By default, it downloads the most recent version of the module. Use the `Version` property to download a specific version. The module is downloaded from any of the repositories that are configured on the current machine. Those repositories must be trusted and initialized, otherwise the module will fail to download. ## Property * `Name` (mandatory): a list of module names you want installed. * `Version`: the version number to download. The default behavior is to download the latest version. Wildcards allowed, e.g. use `2.*` to pin to major version `2`. .EXAMPLE ## Example 1 Build: - GetPowerShellModule: Name: Whiskey This example demonstrates how to download the latest version of a PowerShell module. In this case, the latest version of Whiskey is downloaded and saved into the `.\Modules` directory in your build root. ## Example 2 Build: - GetPowerShellModule: Name: Whiskey Version: "0.14.*" This example demonstrates how to pin to a specific version of a module. In this case, the latest `0.14.x` version will be downloaded. When version 0.15.0 comes out, you'll still download the latest `0.14.x` version. #> [CmdletBinding()] [Whiskey.Task("GetPowerShellModule",SupportsClean=$true, SupportsInitialize=$true)] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $TaskParameter['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 $TaskParameter['Name'] return } $module = Resolve-WhiskeyPowerShellModule -Name $TaskParameter['Name'] -Version $TaskParameter['Version'] -ErrorAction Stop if( -not $module ) { return } Write-WhiskeyInfo -Context $TaskContext -Message ('Installing PowerShell module {0} {1}.' -f $TaskParameter['Name'],$module.Version) $moduleRoot = Install-WhiskeyPowerShellModule -Name $TaskParameter['Name'] -Version $module.Version -ErrorAction Stop Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $moduleRoot) } function New-WhiskeyGitHubRelease { [CmdletBinding()] [Whiskey.Task('GitHubRelease')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [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=$true)] [uri] $Uri, [Parameter(Mandatory=$true,ParameterSetName='FileUpload')] [string] $ContentType, [Parameter(Mandatory=$true,ParameterSetName='FileUpload')] [string] $InFile, [Parameter(Mandatory=$true,ParameterSetName='JsonRequest')] $Parameter, [Microsoft.PowerShell.Commands.WebRequestMethod] $Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::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 { 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 } $releaseData = @{ tag_name = $tag } if( $TaskParameter['Commitish'] ) { $releaseData['target_commitish'] = $TaskParameter['Commitish'] } if( $TaskParameter['Name'] ) { $releaseData['name'] = $TaskParameter['Name'] } if( $TaskParameter['Description'] ) { $releaseData['body'] = $TaskParameter['Description'] } Write-WhiskeyInfo -Context $TaskContext -Message ('{0} release "{1}" "{2}" at commit "{3}".' -f $actionDescription,$TaskParameter['Name'],$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 if( -not $assetPath ) { continue } $assetName = $assetPath | Split-Path -Leaf $assetLabel = $asset['Name'] if( $assetLabel -eq $null ) { $assetLabel = "" } $existingAsset = $existingAssets | Where-Object { $_ -and $_.name -eq $assetName } if( $existingAsset ) { Write-WhiskeyInfo -Context $TaskContext -Message ('Updating file "{0}".' -f $assetName) Invoke-GitHubApi -Method Patch -Uri $existingAsset.url -Parameter @{ name = $assetName; label = $assetLabel } } else { $uri = $release.upload_url -replace '{[^}]+}$' $uri = '{0}?name={1}' -f $uri,[uri]::EscapeDataString($assetName) if( $assetLabel ) { $uri = '{0}&label={1}' -f $uri,[uri]::EscapeDataString($assetLabel) } Write-WhiskeyInfo -Context $TaskContext -Message ('Uploading file "{0}".' -f $assetPath) $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 } Invoke-GitHubApi -Method Post -Uri $uri -ContentType $asset['ContentType'] -InFile $assetPath } } } } function Import-WhiskeyTask { [Whiskey.Task("LoadTask")] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $module = Get-Module -Name 'Whiskey' $paths = Resolve-WhiskeyTaskPath -TaskContext $TaskContext -Path $TaskParameter['Path'] -PropertyName 'Path' foreach( $path in $paths ) { if( $TaskContext.TaskPaths | Where-Object { $_.FullName -eq $path } ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Already loaded tasks from file "{0}".' -f $path) -Verbose 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. & { . $path } $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 $path) return } Write-WhiskeyInfo -Context $TaskContext -Message ($path) foreach( $task in $newTasks ) { Write-WhiskeyInfo -Context $TaskContext -Message $task.Name -Indent 1 } $TaskContext.TaskPaths.Add((Get-Item -Path $path)) } } function Merge-WhiskeyFile { [CmdletBinding()] [Whiskey.Task('MergeFile')] param( [Parameter(Mandatory)] [Whiskey.Context]$TaskContext, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')] [string[]]$Path, [string]$DestinationPath, [switch]$DeleteSourceFiles, [string]$TextSeparator, [byte[]]$BinarySeparator, [switch]$Clear, [string[]]$Exclude ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $DestinationPath = Resolve-WhiskeyTaskPath -TaskContext $TaskContext ` -Path $DestinationPath ` -PropertyName 'DestinationPath' ` -PathType 'File' ` -Force $normalizedBuildRoot = $TaskContext.BuildRoot.FullName.TrimEnd([IO.Path]::DirectorySeparatorChar,[IO.Path]::AltDirectorySeparatorChar) $normalizedBuildRoot = Join-Path -Path $normalizedBuildRoot -ChildPath ([IO.Path]::DirectorySeparatorChar) if( -not $DestinationPath.StartsWith($normalizedBuildRoot) ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'DestinationPath' -Message ('"{0}" resolves to "{1}", which is outside the build root "{2}".' -f $PSBoundParameters['DestinationPath'],$DestinationPath,$TaskContext.BuildRoot.FullName) return } 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-Debug -Message ('"{0}" -notlike "{1}"' -f $filePath,$pattern) } } if( $excluded ) { continue } $relativePath = Resolve-Path -Path $filePath -Relative Write-WhiskeyInfo -Context $TaskContext -Message (' + {0}' -f $relativePath) -Verbose 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.RequiresTool('PowerShellModule::VSSetup','VSSetupPath',Version='2.*',VersionParameterName='VSSetupVersion')] [CmdletBinding()] param( [Whiskey.Context] # The context this task is operating in. Use `New-WhiskeyContext` to create context objects. $TaskContext, [hashtable] # The parameters/configuration to use to run the task. Should be a hashtable that contains the following item(s): # # * `Path` (Mandatory): the relative paths to the files/directories to include in the build. Paths should be relative to the whiskey.yml file they were taken from. $TaskParameter ) Set-StrictMode -version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Import-WhiskeyPowerShellModule -Name 'VSSetup' #setup $nuGetPath = Install-WhiskeyNuGet -DownloadRoot $TaskContext.BuildRoot -Version $TaskParameter['NuGetVersion'] # Make sure the Taskpath contains a Path parameter. if( -not ($TaskParameter.ContainsKey('Path')) -or -not $TaskParameter['Path'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Element ''Path'' is mandatory. It should be one or more paths, relative to your whiskey.yml file, to build with MSBuild.exe, e.g. Build: - MSBuild: Path: - MySolution.sln - MyCsproj.csproj') return } $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' $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'] } } foreach( $projectPath in $path ) { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $projectPath) $errors = $null 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-WhiskeyVerbose -Context $TaskContext -Message (' Restoring NuGet packages.') & $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 { ('Configuration={0}' -f $configuration) if( $TaskParameter.ContainsKey('Property') ) { $TaskParameter['Property'] } if( $TaskParameter.ContainsKey('OutputDirectory') ) { ('OutDir={0}' -f ($TaskParameter['OutputDirectory'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'OutputDirectory' -Force)) } } $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 = $property | ForEach-Object { $item = $_ $name,$value = $item -split '=',2 $value = $value.Trim('"') $value = $value.Trim("'") if( $value.EndsWith( '\' ) ) { $value = '{0}\' -f $value } '/p:{0}="{1}"' -f $name,($value -replace ' ','%20') } $targetArg = '/t:{0}' -f ($target -join ';') & $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','NodePath')] [Whiskey.RequiresTool('NodeModule::license-checker','LicenseCheckerPath',VersionParameterName='LicenseCheckerVersion')] [Whiskey.RequiresTool('NodeModule::nsp','NspPath',VersionParameterName='PINNED_TO_NSP_2_7_0',Version='2.7.0')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] # The context the task is running under. $TaskContext, [Parameter(Mandatory=$true)] [hashtable] # 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. $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( $TaskContext.ShouldClean ) { Write-WhiskeyTiming -Message 'Cleaning' $nodeModulesPath = Join-Path -Path $TaskContext.BuildRoot -ChildPath 'node_modules' Remove-WhiskeyFileSystemItem -Path $nodeModulesPath Write-WhiskeyTiming -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=$true)] [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-WhiskeyTiming -Message ('npm install') Invoke-WhiskeyNpmCommand -Name 'install' -ArgumentList '--production=false' -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop Write-WhiskeyTiming -Message ('COMPLETE') if( $TaskContext.ShouldInitialize ) { Write-WhiskeyTiming -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-WhiskeyTiming -Message ('Running script ''{0}''.' -f $script) Invoke-WhiskeyNpmCommand -Name 'run-script' -ArgumentList $script -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop Write-WhiskeyTiming -Message ('COMPLETE') } $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $TaskContext.BuildRoot Update-Progress -Status ('nsp check') -Step ($stepNum++) Write-WhiskeyTiming -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-WhiskeyTiming -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-WhiskeyTiming -Message ('Generating license report.') $licenseCheckerPath = Assert-WhiskeyNodeModulePath -Path $TaskParameter['LicenseCheckerPath'] -CommandPath 'bin\license-checker' -ErrorAction Stop $reportJson = & $nodePath $licenseCheckerPath '--json' Write-WhiskeyTiming -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-WhiskeyTiming -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-WhiskeyTiming -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', 'NodePath',VersionParameterName='NodeVersion')] [Whiskey.RequiresTool('NodeModule::license-checker', 'LicenseCheckerPath', VersionParameterName='Version')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) 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-WhiskeyTiming -Message ('Generating license report') $reportJson = Invoke-Command -NoNewScope -ScriptBlock { & $nodePath $licenseCheckerPath '--json' } Write-WhiskeyTiming -Message ('COMPLETE') $report = Invoke-Command -NoNewScope -ScriptBlock { ($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-WhiskeyTiming -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-WhiskeyTiming -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', 'NodePath', VersionParameterName='NodeVersion')] [Whiskey.RequiresTool('NodeModule::nsp', 'NspPath', VersionParameterName='Version')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [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-WhiskeyTiming -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-WhiskeyTiming -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','NodePath',VersionParameterName='NodeVersion')] [Whiskey.RequiresTool('NodeModule::npm','NpmPath',VersionParameterName='NpmVersion')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [object] $TaskContext, [Parameter(Mandatory=$true)] [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','NodePath',VersionParameterName='NodeVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $configuration = $TaskParameter['Configuration'] if( -not $configuration ) { Write-Warning -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', 'NodePath',VersionParameterName='NodeVersion')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [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-WhiskeyTiming -Message 'Removing project node_modules' Remove-WhiskeyFileSystemItem -Path 'node_modules' -ErrorAction Stop } else { Write-WhiskeyTiming -Message 'Installing Node modules' Invoke-WhiskeyNpmCommand -Name 'install' -ArgumentList '--production=false' -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop } Write-WhiskeyTiming -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-WhiskeyTiming -Message ('Uninstalling {0}' -f $packageName) Uninstall-WhiskeyNodeModule -BuildRootPath $TaskContext.BuildRoot ` -Name $packageName ` -ForDeveloper:$TaskContext.ByDeveloper ` -Global:$installGlobally ` -ErrorAction Stop } } else { Write-WhiskeyTiming -Message ('Installing {0}' -f $packageName) Install-WhiskeyNodeModule -BuildRootPath $TaskContext.BuildRoot ` -Name $packageName ` -Version $packageVersion ` -ForDeveloper:$TaskContext.ByDeveloper ` -Global:$installGlobally ` -ErrorAction Stop } Write-WhiskeyTiming -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','NodePath',VersionParameterName='NodeVersion')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] # The context the task is running under. $TaskContext, [Parameter(Mandatory=$true)] [hashtable] # The parameters/configuration to use to run the task. $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','NodePath',VersionParameterName='NodeVersion')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [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-WhiskeyTiming -Message ('Running script ''{0}''.' -f $script) Invoke-WhiskeyNpmCommand -Name 'run-script' -ArgumentList $script -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop Write-WhiskeyTiming -Message ('COMPLETE') } } function New-WhiskeyNuGetPackage { [Whiskey.Task("NuGetPack",Platform='Windows')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState if( -not ($TaskParameter.ContainsKey('Path'))) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''Path'' is mandatory. It should be one or more paths to .csproj or .nuspec files to pack, e.g. Build: - PublishNuGetPackage: Path: - MyProject.csproj - MyNuspec.nuspec ') return } $paths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' $symbols = $TaskParameter['Symbols'] | ConvertFrom-WhiskeyYamlScalar $symbolsArg = '' $symbolsFileNameSuffix = '' if( $symbols ) { $symbolsArg = '-Symbols' $symbolsFileNameSuffix = '.symbols' } $nuGetPath = Install-WhiskeyNuGet -DownloadRoot $TaskContext.BuildRoot -Version $TaskParameter['Version'] if( -not $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 ($path in $paths) { $projectName = $TaskParameter['PackageID'] if( -not $projectName ) { $projectName = [IO.Path]::GetFileNameWithoutExtension(($path | Split-Path -Leaf)) } $packageVersion = $TaskParameter['PackageVersion'] if( -not $packageVersion ) { $packageVersion = $TaskContext.Version.SemVer1 } # Create NuGet package $configuration = Get-WhiskeyMSBuildConfiguration -Context $TaskContext & $nugetPath pack -Version $packageVersion -OutputDirectory $TaskContext.OutputDirectory $symbolsArg -Properties ('Configuration={0}' -f $configuration) $propertiesArgs $path # 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 $path,$packagePath) return } } } function Publish-WhiskeyNuGetPackage { [Whiskey.Task("NuGetPush",Platform='Windows',Aliases=('PublishNuGetLibrary','PublishNuGetPackage'),WarnWhenUsingAlias=$true)] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory)] [hashtable] $TaskParameter, [Whiskey.Tasks.ValidatePath(PathType='File')] [string[]] $Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState if( -not $Path ) { $Path = Join-Path -Path $TaskContext.OutputDirectory.FullName -ChildPath '*.nupkg' | Resolve-Path | Select-Object -ExpandProperty 'ProviderPath' } $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 = Install-WhiskeyNuGet -DownloadRoot $TaskContext.BuildRoot -Version $TaskParameter['Version'] if( -not $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 { 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 $_ Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unknown failure checking if {0} {1} package already exists at {2}. {3}' -f $packageName,$packageVersion,$packageUri,$_) return } $response = $_.Exception.Response if( $response.StatusCode -ne [Net.HttpStatusCode]::NotFound ) { $error = '' if( $response | Get-Member 'GetResponseStream' ) { $content = $response.GetResponseStream() $content.Position = 0 $reader = New-Object 'IO.StreamReader' $content $error = $reader.ReadToEnd() -replace '<[^>]+?>','' $reader.Close() $response.Close() } Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failure checking if {0} {1} package already exists at {2}. The web request returned a {3} ({4}) status code:{5} {5}{6}' -f $packageName,$packageVersion,$packageUri,$response.StatusCode,[int]$response.StatusCode,[Environment]::NewLine,$error) 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 -Uri $source -ApiKey $apiKey -NuGetPath $nuGetPath if( -not ($TaskParameter['SkipUploadedCheck'] | ConvertFrom-WhiskeyYamlScalar) ) { try { 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')] param( [Parameter(Mandatory)] [Whiskey.Tasks.ValidatePath(Mandatory)] [string[]] $Path, [string[]] $Argument, [string] $Version, [Whiskey.Tasks.ParameterValueFromVariable('WHISKEY_BUILD_ROOT')] [IO.DirectoryInfo] $BuildRoot ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $nuGetPath = Install-WhiskeyNuGet -DownloadRoot $BuildRoot -Version $Version foreach( $item in $Path ) { & $nuGetPath 'restore' $item $Argument } } function Invoke-WhiskeyNUnit2Task { [Whiskey.Task("NUnit2",SupportsClean, SupportsInitialize,Platform='Windows')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -version 'latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $package = 'NUnit.Runners' $version = '2.6.4' if( $TaskParameter['Version'] ) { $version = $TaskParameter['Version'] if( $version -notlike '2.*' ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Version' -Message ('The version ''{0}'' isn''t a valid 2.x version of NUnit.' -f $TaskParameter['Version']) return } } $openCoverVersionArg = @{} $reportGeneratorVersionArg = @{} if( $TaskParameter['OpenCoverVersion'] ) { $openCoverVersionArg['Version'] = $TaskParameter['OpenCoverVersion'] } if( $TaskParameter['ReportGeneratorVersion'] ) { $reportGeneratorVersionArg['Version'] = $TaskParameter['ReportGeneratorVersion'] } $openCoverArgs = @() if( $TaskParameter['OpenCoverArgument'] ) { $openCoverArgs += $TaskParameter['OpenCoverArgument'] } $reportGeneratorArgs = @() if( $TaskParameter['ReportGeneratorArgument'] ) { $reportGeneratorArgs += $TaskParameter['ReportGeneratorArgument'] } if( $TaskContext.ShouldClean ) { Write-WhiskeyTiming -Message ('Uninstalling ReportGenerator.') Uninstall-WhiskeyTool -NuGetPackageName 'ReportGenerator' -BuildRoot $TaskContext.BuildRoot @reportGeneratorVersionArg Write-WhiskeyTiming -Message ('COMPLETE') Write-WhiskeyTiming -Message ('Uninstalling OpenCover.') Uninstall-WhiskeyTool -NuGetPackageName 'OpenCover' -BuildRoot $TaskContext.BuildRoot @openCoverVersionArg Write-WhiskeyTiming -Message ('COMPLETE') Write-WhiskeyTiming -Message ('Uninstalling NUnit.') Uninstall-WhiskeyTool -NuGetPackageName $package -BuildRoot $TaskContext.BuildRoot -Version $version Write-WhiskeyTiming -Message ('COMPLETE') return } $includeParam = $null if( $TaskParameter.ContainsKey('Include') ) { $includeParam = '/include={0}' -f $TaskParameter['Include'] } $excludeParam = $null if( $TaskParameter.ContainsKey('Exclude') ) { $excludeParam = '/exclude={0}' -f $TaskParameter['Exclude'] } $frameworkParam = '4.0' if( $TaskParameter.ContainsKey('Framework') ) { $frameworkParam = $TaskParameter['Framework'] } $frameworkParam = '/framework={0}' -f $frameworkParam Write-WhiskeyTiming -Message ('Installing NUnit.') $nunitRoot = Install-WhiskeyTool -NuGetPackageName $package -Version $version -DownloadRoot $TaskContext.BuildRoot Write-WhiskeyTiming -Message ('COMPLETE') if( -not (Test-Path -Path $nunitRoot -PathType Container) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Package {0} {1} failed to install!' -f $package,$version) return } $nunitRoot = Get-Item -Path $nunitRoot | Select-Object -First 1 $nunitRoot = Join-Path -Path $nunitRoot -ChildPath 'tools' $nunitConsolePath = Join-Path -Path $nunitRoot -ChildPath 'nunit-console.exe' -Resolve if( -not ($nunitConsolePath)) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('{0} {1} was installed, but couldn''t find nunit-console.exe at ''{2}''.' -f $package,$version,$nunitConsolePath) return } Write-WhiskeyTiming -Message ('Installing OpenCover.') $openCoverRoot = Install-WhiskeyTool -NuGetPackageName 'OpenCover' -DownloadRoot $TaskContext.BuildRoot @openCoverVersionArg Write-WhiskeyTiming -Message ('COMPLETE') if( -not (Test-Path -Path $openCoverRoot -PathType Container)) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failed to install NuGet package OpenCover {0}.' -f $version) return } $openCoverPath = Get-ChildItem -Path $openCoverRoot -Filter 'OpenCover.Console.exe' -Recurse | Select-Object -First 1 | Select-Object -ExpandProperty 'FullName' if( -not (Test-Path -Path $openCoverPath -PathType Leaf) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find OpenCover.Console.exe in OpenCover NuGet package at ''{0}''.' -f $openCoverRoot) return } Write-WhiskeyTiming -Message ('Installing ReportGenerator.') $reportGeneratorRoot = Install-WhiskeyTool -NuGetPackageName 'ReportGenerator' -DownloadRoot $TaskContext.BuildRoot @reportGeneratorVersionArg Write-WhiskeyTiming -Message ('COMPLETE') if( -not (Test-Path -Path $reportGeneratorRoot -PathType Container)) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failed to install NuGet package ReportGenerator.' -f $version) return } $reportGeneratorPath = Get-ChildItem -Path $reportGeneratorRoot -Filter 'ReportGenerator.exe' -Recurse | Select-Object -First 1 | Select-Object -ExpandProperty 'FullName' if( -not (Test-Path -Path $reportGeneratorPath -PathType Leaf) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find ReportGenerator.exe in ReportGenerator NuGet package at ''{0}''.' -f $reportGeneratorRoot) return } if( $TaskContext.ShouldInitialize ) { return } # Be sure that the Taskparameter contains a 'Path'. if( -not ($TaskParameter.ContainsKey('Path'))) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Element ''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 } $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' $reportPath = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('nunit2+{0}.xml' -f [IO.Path]::GetRandomFileName()) $coverageReportDir = Join-Path -Path $TaskContext.outputDirectory -ChildPath "opencover" New-Item -Path $coverageReportDir -ItemType 'Directory' -Force | Out-Null $openCoverReport = Join-Path -Path $coverageReportDir -ChildPath 'openCover.xml' $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-WhiskeyVerbose -Context $TaskContext -Message (' CoverageFilter {0}' -f ($TaskParameter['CoverageFilter'] | Select-Object -First 1)) $TaskParameter['CoverageFilter'] | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) } Write-WhiskeyVerbose -Context $TaskContext -Message (' Output {0}' -f $openCoverReport) $disableCodeCoverage = $TaskParameter['DisableCodeCoverage'] | ConvertFrom-WhiskeyYamlScalar Write-WhiskeyVerbose -Context $TaskContext -Message (' DisableCodeCoverage {0}' -f $disableCodeCoverage) Write-WhiskeyVerbose -Context $TaskContext -Message (' OpenCoverArgs {0}' -f ($openCoverArgs | Select-Object -First 1)) $openCoverArgs | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) } Write-WhiskeyVerbose -Context $TaskContext -Message (' ReportGeneratorArgs {0}' -f ($reportGeneratorArgs | Select-Object -First 1)) $reportGeneratorArgs | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) } if( -not $disableCodeCoverage ) { $coverageFilterString = ($TaskParameter['CoverageFilter'] -join " ") $extraArgString = ($extraArgs -join " ") $pathsArg = ($path -join '" "') $nunitArgs = '"{0}" /noshadow {1} /xml="{2}" {3} {4} {5}' -f $pathsArg,$frameworkParam,$reportPath,$includeParam,$excludeParam,$extraArgString $nunitArgs = $nunitArgs -replace '"', '\"' Write-WhiskeyTiming -Message ('Running OpenCover') & $openCoverPath "-target:${nunitConsolePath}" "-targetargs:${nunitArgs}" "-filter:${coverageFilterString}" '-register:user' "-output:${openCoverReport}" '-returntargetcode' $openCoverArgs Write-WhiskeyTiming -Message ('COMPLETE') $testsFailed = $LastExitCode; Write-WhiskeyTiming -Message ('Running ReportGenerator') & $reportGeneratorPath "-reports:${openCoverReport}" "-targetdir:$coverageReportDir" $reportGeneratorArgs Write-WhiskeyTiming -Message ('COMPLETE') if( $LastExitCode -or $testsFailed ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NUnit2 tests failed. {0} returned exit code {1}.' -f $openCoverPath,$LastExitCode) return } } else { Write-WhiskeyTiming -Message ('Running NUnit') & $nunitConsolePath $path $frameworkParam $includeParam $excludeParam $extraArgs ('/xml={0}' -f $reportPath) Write-WhiskeyTiming -Message ('COMPLETE') 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",SupportsClean,SupportsInitialize,Platform='Windows')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState # NUnit.Console pulls in ConsoleRunner (of the same version) as a dependency and several NUnit2 compatibility/extension packages. # The ConsoleRunner packages is installed explicitly to resolve the tool/bin path from installed package location. $nunitSupportPackage = 'NUnit.Console' $nunitPackage = 'NUnit.ConsoleRunner' # Due to a bug in NuGet we can't search for and install packages with wildcards (e.g. 3.*), so we're hardcoding a version for now. See Resolve-WhiskeyNuGetPackageVersion for more details. # (This is the vesrion of NUnit.Console/NUnit.ConsoleRunner which may differ from the core NUnit library version.) $nunitVersion = '3.10.0' if( $TaskParameter['Version'] ) { $nunitVersion = $TaskParameter['Version'] if( $nunitVersion -notlike '3.*' ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Version' -Message ('The version ''{0}'' isn''t a valid 3.x version of NUnit.' -f $TaskParameter['Version']) return } } $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 -ChildPath ('{0}+{1}.xml' -f $reportFormat, [IO.Path]::GetRandomFileName()) $nunitReportParam = '--result={0};format={1}' -f $nunitReport, $reportFormat $openCoverVersionParam = @{} if ($TaskParameter['OpenCoverVersion']) { $openCoverVersionParam['Version'] = $TaskParameter['OpenCoverVersion'] } $reportGeneratorVersionParam = @{} if ($TaskParameter['ReportGeneratorVersion']) { $reportGeneratorVersionParam['Version'] = $TaskParameter['ReportGeneratorVersion'] } if( $TaskContext.ShouldClean ) { Uninstall-WhiskeyTool -NuGetPackageName $nunitSupportPackage -BuildRoot $TaskContext.BuildRoot -Version $nunitVersion Uninstall-WhiskeyTool -NuGetPackageName $nunitPackage -BuildRoot $TaskContext.BuildRoot -Version $nunitVersion Uninstall-WhiskeyTool -NuGetPackageName 'OpenCover' -BuildRoot $TaskContext.BuildRoot @openCoverVersionParam Uninstall-WhiskeyTool -NuGetPackageName 'ReportGenerator' -BuildRoot $TaskContext.BuildRoot @reportGeneratorVersionParam return } $nunitSupportPath = Install-WhiskeyTool -NuGetPackageName $nunitSupportPackage -Version $nunitVersion -DownloadRoot $TaskContext.BuildRoot if (-not $nunitSupportPath) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Package "{0}" failed to install.' -f $nunitSupportPackage) return } $nunitPath = Install-WhiskeyTool -NuGetPackageName $nunitPackage -Version $nunitVersion -DownloadRoot $TaskContext.BuildRoot if (-not $nunitPath) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Package "{0}" failed to install.' -f $nunitPackage) return } $openCoverPath = Install-WhiskeyTool -NuGetPackageName 'OpenCover' -DownloadRoot $TaskContext.BuildRoot @openCoverVersionParam if (-not $openCoverPath) { Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Package "OpenCover" failed to install.' return } $reportGeneratorPath = Install-WhiskeyTool -NuGetPackageName 'ReportGenerator' -DownloadRoot $TaskContext.BuildRoot @reportGeneratorVersionParam if (-not $reportGeneratorPath) { Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Package "ReportGenerator" failed to install.' return } if( $TaskContext.ShouldInitialize ) { return } $openCoverArgument = @() if ($TaskParameter['OpenCoverArgument']) { $openCoverArgument = $TaskParameter['OpenCoverArgument'] } $reportGeneratorArgument = @() if ($TaskParameter['ReportGeneratorArgument']) { $reportGeneratorArgument = $TaskParameter['ReportGeneratorArgument'] } $framework = '4.0' if ($TaskParameter['Framework']) { $framework = $TaskParameter['Framework'] } $frameworkParam = '--framework={0}' -f $framework $testFilter = '' $testFilterParam = '' if ($TaskParameter['TestFilter']) { $testFilter = $TaskParameter['TestFilter'] | ForEach-Object { '({0})' -f $_ } $testFilter = $testFilter -join ' or ' $testFilterParam = '--where={0}' -f $testFilter } $nunitExtraArgument = '' if ($TaskParameter['Argument']) { $nunitExtraArgument = $TaskParameter['Argument'] } $disableCodeCoverage = $TaskParameter['DisableCodeCoverage'] | ConvertFrom-WhiskeyYamlScalar $coverageFilter = '' if ($TaskParameter['CoverageFilter']) { $coverageFilter = $TaskParameter['CoverageFilter'] -join ' ' } $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 } $openCoverConsolePath = Get-ChildItem -Path $openCoverPath -Filter 'OpenCover.Console.exe' -Recurse | Select-Object -First 1 | Select-Object -ExpandProperty 'FullName' if( -not $openCoverConsolePath ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find "OpenCover.Console.exe" in OpenCover NuGet package at "{0}".' -f $openCoverPath) return } $reportGeneratorConsolePath = Get-ChildItem -Path $reportGeneratorPath -Filter 'ReportGenerator.exe' -Recurse | Select-Object -First 1 | Select-Object -ExpandProperty 'FullName' if( -not $reportGeneratorConsolePath ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find "ReportGenerator.exe" in ReportGenerator NuGet package at "{0}".' -f $reportGeneratorPath) return } if (-not $TaskParameter['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 } $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' $path | Foreach-Object { if (-not (Test-Path -Path $_ -PathType Leaf)) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('''Path'' item ''{0}'' does not exist.' -f $_) return } } $coverageReportDir = Join-Path -Path $TaskContext.outputDirectory -ChildPath "opencover" New-Item -Path $coverageReportDir -ItemType 'Directory' -Force | Out-Null $openCoverReport = Join-Path -Path $coverageReportDir -ChildPath 'openCover.xml' $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) Write-WhiskeyVerbose -Context $TaskContext -Message (' CoverageFilter {0}' -f $coverageFilter) Write-WhiskeyVerbose -Context $TaskContext -Message (' OpenCover Report {0}' -f $openCoverReport) Write-WhiskeyVerbose -Context $TaskContext -Message (' DisableCodeCoverage {0}' -f $disableCodeCoverage) Write-WhiskeyVerbose -Context $TaskContext -Message (' OpenCoverArgs {0}' -f ($openCoverArgument -join ' ')) Write-WhiskeyVerbose -Context $TaskContext -Message (' ReportGeneratorArgs {0}' -f ($reportGeneratorArgument -join ' ')) $nunitExitCode = 0 $reportGeneratorExitCode = 0 $openCoverExitCode = 0 $openCoverExitCodeOffset = 1000 if (-not $disableCodeCoverage) { $path = $path | ForEach-Object { '\"{0}\"' -f $_ } $path = $path -join ' ' $nunitReportParam = '\"{0}\"' -f $nunitReportParam if ($frameworkParam) { $frameworkParam = '\"{0}\"' -f $frameworkParam } if ($testFilterParam) { $testFilterParam = '\"{0}\"' -f $testFilterParam } if ($nunitExtraArgument) { $nunitExtraArgument = $nunitExtraArgument | ForEach-Object { '\"{0}\"' -f $_ } $nunitExtraArgument = $nunitExtraArgument -join ' ' } $openCoverNunitArguments = '{0} {1} {2} {3} {4}' -f $path,$frameworkParam,$testFilterParam,$nunitReportParam,$nunitExtraArgument & $openCoverConsolePath "-target:$nunitConsolePath" "-targetargs:$openCoverNunitArguments" "-filter:$coverageFilter" "-output:$openCoverReport" -register:user -returntargetcode:$openCoverExitCodeOffset $openCoverArgument if ($LASTEXITCODE -ge 745) { $openCoverExitCode = $LASTEXITCODE - $openCoverExitCodeOffset } else { $nunitExitCode = $LASTEXITCODE } & $reportGeneratorConsolePath "-reports:$openCoverReport" "-targetdir:$coverageReportDir" $reportGeneratorArgument $reportGeneratorExitCode = $LASTEXITCODE } else { & $nunitConsolePath $path $frameworkParam $testFilterParam $nunitReportParam $nunitExtraArgument $nunitExitCode = $LASTEXITCODE } if ($reportGeneratorExitCode -ne 0) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('ReportGenerator didn''t run successfully. ''{0}'' returned exit code ''{1}''.' -f $reportGeneratorConsolePath,$reportGeneratorExitCode) return } elseif ($openCoverExitCode -ne 0) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('OpenCover didn''t run successfully. ''{0}'' returned exit code ''{1}''.' -f $openCoverConsolePath, $openCoverExitCode) return } elseif ($nunitExitCode -ne 0) { if (-not (Test-Path -Path $nunitReport -PathType Leaf)) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NUnit3 didn''t run successfully. ''{0}'' returned exit code ''{1}''.' -f $nunitConsolePath,$nunitExitCode) return } else { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NUnit3 tests failed. ''{0}'' returned exit code ''{1}''.' -f $nunitConsolePath,$nunitExitCode) return } } } function Invoke-WhiskeyParallelTask { [CmdletBinding()] [Whiskey.Task('Parallel')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) 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 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 $tasks = $queue['Tasks'] | ForEach-Object { $taskName,$taskParameter = ConvertTo-WhiskeyTask -InputObject $_ -ErrorAction Stop [pscustomobject]@{ Name = $taskName; Parameter = $taskParameter } } $job = Start-Job -Name $queueIdx -ScriptBlock { Set-StrictMode -Version 'Latest' $VerbosePreference = $using:VerbosePreference $DebugPreference = $using:DebugPreference $ProgressPreference = $using:ProgressPreference $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-Verbose ('Loading tasks from "{0}".' -f $info.FullName) . $info.FullName } foreach( $task in $using:tasks ) { Write-Debug -Message ($task.Name) $task.Parameter | ConvertTo-Json -Depth 50 | Write-Debug Invoke-WhiskeyTask -TaskContext $context -Name $task.Name -Parameter $task.Parameter } } $job | Add-Member -MemberType NoteProperty -Name 'QueueIndex' -Value $queueIdx -PassThru | Add-Member -MemberType NoteProperty -Name 'Completed' -Value $false [void]$jobs.Add($job) } $lastNotice = (Get-Date).AddSeconds(-61) while( $jobs | Where-Object { -not $_.Completed } ) { foreach( $job in $jobs ) { if( $job.Completed ) { continue } if( $lastNotice -lt (Get-Date).AddSeconds(-60) ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}][{1}] Waiting for queue.' -f $job.QueueIndex,$job.Name) $notified = $true } $completedJob = $job | Wait-Job -Timeout 1 if( $completedJob ) { $job.Completed = $true $completedJob | Receive-Job $duration = $job.PSEndTime - $job.PSBeginTime Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}][{1}] {2} in {3}' -f $job.QueueIndex,$job.Name,$job.State.ToString().ToUpperInvariant(),$duration) if( $job.JobStateInfo.State -eq [Management.Automation.JobState]::Failed ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Queue[{0}] failed. See previous output for error information.' -f $job.Name) return } } } if( $notified ) { $notified = $false $lastNotice = Get-Date } } } finally { $jobs | Stop-Job $jobs | Remove-Job } } function Invoke-WhiskeyPester3Task { [Whiskey.Task('Pester3',Platform='Windows')] [Whiskey.RequiresTool('PowerShellModule::Pester','PesterPath',Version='3.*',VersionParameterName='Version')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not ($TaskParameter.ContainsKey('Path'))) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Element ''Path'' is mandatory. It should be one or more paths, which should be a list of Pester Tests to run with Pester3, e.g. Build: - Pester3: Path: - My.Tests.ps1 - Tests') return } $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' $outputFile = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('pester+{0}.xml' -f [IO.Path]::GetRandomFileName()) $outputFile = [IO.Path]::GetFullPath($outputFile) # 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 $script = $using:Path $pesterModulePath = $using:TaskParameter['PesterPath'] $outputFile = $using:outputFile Invoke-Command -ScriptBlock { $VerbosePreference = 'SilentlyContinue' Import-Module -Name $pesterModulePath } 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. $informationActionParameter = @{ } if( (Get-Command -Name 'Receive-Job' -ParameterName 'InformationAction') ) { $informationActionParameter['InformationAction'] = 'SilentlyContinue' } do { $job | Receive-Job @informationActionParameter } while( -not ($job | Wait-Job -Timeout 1) ) $job | Receive-Job @informationActionParameter 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.RequiresTool('PowerShellModule::Pester','PesterPath',Version='4.*',VersionParameterName='Version')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not ($TaskParameter.ContainsKey('Path'))) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Path" is mandatory. It should be one or more paths, which should be a list of Pester test scripts (e.g. Invoke-WhiskeyPester4Task.Tests.ps1) or directories that contain Pester test scripts, e.g. Build: - Pester4: Path: - My.Tests.ps1 - Tests') return } $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' if( $TaskParameter['Exclude'] ) { $path = $path | Where-Object { foreach( $exclusion in $TaskParameter['Exclude'] ) { if( $_ -like $exclusion ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('EXCLUDE {0} -like {1}' -f $_,$exclusion) return $false } else { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0} -notlike {1}' -f $_,$exclusion) } } return $true } if( -not $path ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Found no tests to run. Property "Exclude" matched all paths in the "Path" property. Please update your exclusion rules to include at least one test. View verbose output to see what exclusion filters excluded what test files.') return } } [int]$describeDurationCount = 0 $describeDurationCount = $TaskParameter['DescribeDurationReportCount'] [int]$itDurationCount = 0 $itDurationCount = $TaskParameter['ItDurationReportCount'] $outputFile = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('pester+{0}.xml' -f [IO.Path]::GetRandomFileName()) Write-WhiskeyVerbose -Context $TaskContext -Message $TaskParameter['PesterPath'] Write-WhiskeyVerbose -Context $TaskContext -Message (' Script {0}' -f ($Path | Select-Object -First 1)) $Path | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) } Write-Verbose -Message (' OutputFile {0}' -f $outputFile) # 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 $script = $using:Path $pesterModulePath = $using:TaskParameter['PesterPath'] $outputFile = $using:outputFile [int]$describeCount = $using:describeDurationCount [int]$itCount = $using:itDurationCount Invoke-Command -ScriptBlock { $VerbosePreference = 'SilentlyContinue' Import-Module -Name $pesterModulePath } $result = Invoke-Pester -Script $script -OutputFile $outputFile -OutputFormat NUnitXml -PassThru $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 $describeCount | Format-Table -AutoSize $result.TestResult | Sort-Object -Property 'Time' -Descending | Select-Object -First $itCount | Format-Table -AutoSize -Property 'Describe','Name','Time' } # 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. $informationActionParameter = @{ } if( (Get-Command -Name 'Receive-Job' -ParameterName 'InformationAction') ) { $informationActionParameter['InformationAction'] = 'SilentlyContinue' } do { $job | Receive-Job @informationActionParameter } while( -not ($job | Wait-Job -Timeout 1) ) $job | Receive-Job @informationActionParameter 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-WhiskeyPipelineTask { [CmdletBinding()] [Whiskey.Task("Pipeline", SupportsClean=$true, SupportsInitialize=$true)] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [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=$true,SupportsInitialize=$true)] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not ($TaskParameter.ContainsKey('Path')) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''Path'' is mandatory. It should be one or more paths, which should be a list of PowerShell Scripts to run, e.g. Build: - PowerShell: Path: - myscript.ps1 - myotherscript.ps1 WorkingDirectory: bin') return } $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' $workingDirectory = (Get-Location).ProviderPath $argument = $TaskParameter['Argument'] if( -not $argument ) { $argument = @{ } } foreach( $scriptPath in $path ) { if( -not (Test-Path -Path $WorkingDirectory -PathType Container) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Can''t run PowerShell script ''{0}'': working directory ''{1}'' doesn''t exist.' -f $scriptPath,$WorkingDirectory) continue } $scriptCommand = Get-Command -Name $scriptPath -ErrorAction Ignore if( -not $scriptCommand ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Can''t run PowerShell script ''{0}'': it has a syntax error.' -f $scriptPath) 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 { $VerbosePreference = $using:VerbosePreference $DebugPreference = $using:DebugPreference $ProgressPreference = $using:ProgressPreference $WarningPreference = $using:WarningPreference $ErrorActionPreference = $using:ErrorActionPreference $workingDirectory = $using:WorkingDirectory $scriptPath = $using:ScriptPath $argument = $using:argument $serializedContext = $using:serializableContext $whiskeyScriptRoot = $using:whiskeyScriptRoot $resultPath = $using:resultPath $passTaskContext = $using:passTaskContext Invoke-Command -ScriptBlock { $VerbosePreference = 'SilentlyContinue'; & (Join-Path -Path $whiskeyScriptRoot -ChildPath 'Import-Whiskey.ps1' -Resolve -ErrorAction Stop) } [Whiskey.Context]$context = $serializedContext | ConvertTo-WhiskeyContext $contextArgument = @{ } if( $passTaskContext ) { $contextArgument['TaskContext'] = $context } Set-Location $workingDirectory $Global:LASTEXITCODE = 0 & $scriptPath @contextArgument @argument $result = @{ 'ExitCode' = $Global:LASTEXITCODE 'Successful' = $? } $result | ConvertTo-Json | Set-Content -Path $resultPath } do { $job | Receive-Job } while( -not ($job | Wait-Job -Timeout 1) ) $job | Receive-Job if( (Test-Path -Path $resultPath -PathType Leaf) ) { $runResult = Get-Content -Path $resultPath -Raw | ConvertFrom-Json } else { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('PowerShell script ''{0}'' threw a terminating exception.' -F $scriptPath) return } if( $runResult.ExitCode -ne 0 ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('PowerShell script ''{0}'' failed, exited with code {1}.' -F $scriptPath,$runResult.ExitCode) return } elseif( $runResult.Successful -eq $false ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('PowerShell script ''{0}'' threw a terminating exception.' -F $scriptPath) return } } } function New-WhiskeyProGetUniversalPackage { [CmdletBinding()] [Whiskey.Task("ProGetUniversalPackage")] [Whiskey.RequiresTool('PowerShellModule::ProGetAutomation','ProGetAutomationPath',Version='0.9.*',VersionParameterName='ProGetAutomationVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Import-WhiskeyPowerShellModule -Name 'ProGetAutomation' $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'] # ProGet uses build metadata to distinguish different versions, so we can't use a full semantic version. if( $version ) { if( ($version -notmatch '^\d+\.\d+\.\d+$') ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Version" is invalid. It must be a three part version number, i.e. MAJOR.MINOR.PATCH.') return } [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 } $semVer = New-Object 'SemVersion.SemanticVersion' $semVer.Major,$semVer.Minor,$semVer.Patch,$TaskContext.Version.SemVer2.Prerelease,$TaskContext.Version.SemVer2.Build $version = New-WhiskeyVersionObject -SemVer $semVer } else { $version = $TaskContext.Version } $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-Warning -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) } } $parentPathParam = @{ } $sourceRoot = $TaskContext.BuildRoot if( $TaskParameter.ContainsKey('SourceRoot') ) { $sourceRoot = $TaskParameter['SourceRoot'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'SourceRoot' $parentPathParam['ParentPath'] = $sourceRoot } $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 function Copy-ToPackage { param( [Parameter(Mandatory=$true)] [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 @parentPathParam if( -not $sourcePaths ) { return } foreach( $sourcePath in $sourcePaths ) { $relativePath = $sourcePath -replace ('^{0}' -f ([regex]::Escape($sourceRoot))),'' $relativePath = $relativePath.Trim([IO.Path]::DirectorySeparatorChar) $addParams = @{ BasePath = $sourceRoot } $overrideInfo = '' if( $override ) { $addParams = @{ PackageItemName = $destinationItemName } $overrideInfo = ' -> {0}' -f $destinationItemName } $addParams['CompressionLevel'] = $compressionLevel if( $AsThirdPartyItem ) { Write-WhiskeyInfo -Context $TaskContext -Message (' packaging unfiltered item {0}{1}' -f $relativePath,$overrideInfo) Get-Item -Path $sourcePath | Add-ProGetUniversalPackageFile -PackagePath $outFile @addParams -ErrorAction Stop continue } if( (Test-Path -Path $sourcePath -PathType Leaf) ) { Write-WhiskeyInfo -Context $TaskContext -Message (' packaging file {0}{1}' -f $relativePath,$overrideInfo) Add-ProGetUniversalPackageFile -PackagePath $outFile -InputObject $sourcePath @addParams -ErrorAction Stop continue } if( -not $TaskParameter['Include'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Include" is mandatory because "{0}" 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.' -f $sourcePath) return } 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 ) { $addParams['BasePath'] = $sourcePath $addParams.Remove('PackageItemName') $overrideInfo = ' -> {0}' -f $destinationItemName if ($destinationItemName -ne '.') { $addParams['PackageParentPath'] = $destinationItemName } } Write-WhiskeyInfo -Context $TaskContext -Message (' packaging filtered directory {0}{1}' -f $relativePath,$overrideInfo) Find-Item -Path $sourcePath | Add-ProGetUniversalPackageFile -PackagePath $outFile @addParams -ErrorAction Stop } } } $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 $version.SemVer2NoBuildMetadata.ToString() ` -Name $TaskParameter['Name'] ` -Description $TaskParameter['Description'] ` -AdditionalMetadata $manifestProperties Add-ProGetUniversalPackageFile -PackagePath $outFile -InputObject $versionJsonPath -ErrorAction Stop if( $TaskParameter['Path'] ) { Copy-ToPackage -Path $TaskParameter['Path'] } if( $TaskParameter.ContainsKey('ThirdPartyPath') -and $TaskParameter['ThirdPartyPath'] ) { Copy-ToPackage -Path $TaskParameter['ThirdPartyPath'] -AsThirdPartyItem } } function Publish-WhiskeyBBServerTag { [CmdletBinding()] [Whiskey.Task('PublishBitbucketServerTag')] [Whiskey.RequiresTool('PowerShellModule::BitbucketServerAutomation','BitbucketServerAutomationPath',Version='0.9.*',VersionParameterName='BitbucketServerAutomationVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Import-WhiskeyPowerShellModule -Name 'BitbucketServerAutomation' $exampleTask = 'Publish: - PublishBitbucketServerTag: CredentialID: BitbucketServerCredential Uri: https://bitbucketserver.example.com' 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 Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}] [{1}] [{2}] {3} -> {4}' -f $TaskParameter['Uri'],$projectKey,$repoKey,$commitHash,$tag) New-BBServerTag -Connection $connection -ProjectKey $projectKey -force -RepositoryKey $repoKey -Name $tag -CommitID $commitHash -ErrorAction Stop } function Publish-WhiskeyBuildMasterPackage { [CmdletBinding()] [Whiskey.Task("PublishBuildMasterPackage")] [Whiskey.RequiresTool('PowerShellModule::BuildMasterAutomation','BuildMasterAutomationPath',Version='0.6.*',VersionParameterName='BuildMasterAutomationVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Import-WhiskeyPowerShellModule -Name 'BuildMasterAutomation' $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", "NodePath", VersionParameterName='NodeVersion')] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Whiskey.Context] # The context the task is running under. $TaskContext, [Parameter(Mandatory=$true)] [hashtable] # The parameters/configuration to use to run the task. Should be a hashtable that contains the following item: # # * `WorkingDirectory` (Optional): Provides the default root directory for the NPM `publish` task. Defaults to the directory where the build's `whiskey.yml` file was found. Must be relative to the `whiskey.yml` file. $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $workingDirectory = (Get-Location).ProviderPath $npmRegistryUri = [uri]$TaskParameter['NpmRegistryUri'] 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 (!$TaskContext.Publish) { return } $npmConfigPrefix = '//{0}{1}:' -f $npmregistryUri.Authority,$npmRegistryUri.LocalPath $credentialID = $TaskParameter['CredentialID'] 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 $npmEmail = $TaskParameter['EmailAddress'] if( -not $npmEmail ) { 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 } $npmCredPassword = $credential.GetNetworkCredential().Password $npmBytesPassword = [System.Text.Encoding]::UTF8.GetBytes($npmCredPassword) $npmPassword = [System.Convert]::ToBase64String($npmBytesPassword) 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, $npmEmail) 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 Invoke-WhiskeyNpmCommand -Name 'prune' -ArgumentList '--production' -BuildRootPath $TaskContext.BuildRoot -ErrorAction Stop Invoke-WhiskeyNpmCommand -Name 'publish' -BuildRootPath $TaskContext.BuildRoot -ErrorAction Stop } finally { if (Test-Path $packageNpmrc) { Write-WhiskeyVerbose -Context $TaskContext -Message ('Removing .npmrc at {0}.' -f $packageNpmrc) Remove-Item -Path $packageNpmrc } } } function Publish-WhiskeyPowerShellModule { [Whiskey.Task("PublishPowerShellModule")] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory)] [hashtable] $TaskParameter, [Whiskey.Tasks.ValidatePath(Mandatory,PathType='Directory')] [string] $Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState if( -not $TaskParameter.ContainsKey('RepositoryName') ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "RepositoryName" is mandatory. It should be the name of the PowerShell repository you want to publish to, e.g. Build: - PublishPowerShellModule: Path: mymodule RepositoryName: PSGallery ') return } $repositoryName = $TaskParameter['RepositoryName'] $apiKeyID = $TaskParameter['ApiKeyID'] if( -not $apiKeyID ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "ApiKeyID" is mandatory. It must be the ID of the API key to use when publishing to the "{0}" repository. Use the `Add-WhiskeyApiKey` function to add API keys to the build.' -f $repositoryName) return } $apiKey = Get-WhiskeyApiKey -Context $TaskContext -ID $apiKeyID -PropertyName 'ApiKeyID' $manifestPath = '{0}\{1}.psd1' -f $Path,($Path | Split-Path -Leaf) if( $TaskParameter.ContainsKey('ModuleManifestPath') ) { $manifestPath = $TaskParameter['ModuleManifestPath'] } if( -not (Test-Path -Path $manifestPath -PathType Leaf) ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Module manifest path "{0}" either does not exist or is a directory.' -f $manifestPath) return } $manifest = Get-Content $manifestPath $versionString = 'ModuleVersion = ''{0}.{1}.{2}''' -f ( $TaskContext.Version.SemVer2.Major, $TaskContext.Version.SemVer2.Minor, $TaskContext.Version.SemVer2.Patch ) $manifest = $manifest -replace 'ModuleVersion\s*=\s*(''|")[^''"]*(''|")', $versionString $prereleaseString = 'Prerelease = ''{0}''' -f $TaskContext.Version.SemVer2.Prerelease $manifest = $manifest -replace 'Prerelease\s*=\s*(''|")[^''"]*(''|")', $prereleaseString $manifest | Set-Content $manifestPath Import-WhiskeyPowerShellModule -Name 'PackageManagement','PowerShellGet' $commonParams = @{} if( $VerbosePreference -in @('Continue','Inquire') ) { $commonParams['Verbose'] = $true } if( $DebugPreference -in @('Continue','Inquire') ) { $commonParams['Debug'] = $true } if( (Test-Path -Path 'variable:InformationPreference') ) { $commonParams['InformationAction'] = $InformationPreference } Get-PackageProvider -Name 'NuGet' -ForceBootstrap @commonParams | Out-Null if( -not (Get-PSRepository -Name $repositoryName -ErrorAction Ignore @commonParams) ) { $publishLocation = $TaskParameter['RepositoryUri'] if( -not $publishLocation ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "RepositoryUri" is mandatory since there is no registered repository named "{0}". The "RepositoryUri" must be the URI to the PowerShall repository to publish to. The repository will be registered for you.' -f $repositoryName) return } $credentialParam = @{ } if( $TaskParameter.ContainsKey('CredentialID') ) { $credentialParam['Credential'] = Get-WhiskeyCredential -Context $TaskContext -ID $TaskParameter['CredentialID'] -PropertyName 'CredentialID' } Register-PSRepository -Name $repositoryName -SourceLocation $publishLocation -PublishLocation $publishLocation -InstallationPolicy Trusted -PackageManagementProvider NuGet @credentialParam -ErrorAction Stop @commonParams } # Use the Force switch to allow publishing versions that come *before* the latest version. Publish-Module -Path $Path ` -Repository $repositoryName ` -NuGetApiKey $apiKey ` -Force ` -ErrorAction Stop ` @commonParams } function Publish-WhiskeyProGetAsset { [Whiskey.Task("PublishProGetAsset")] [Whiskey.RequiresTool('PowerShellModule::ProGetAutomation','ProGetAutomationPath',Version='0.9.*',VersionParameterName='ProGetAutomationVersion')] [CmdletBinding()] param( [Whiskey.Context] # The context this task is operating in. Use `New-WhiskeyContext` to create context objects. $TaskContext, [hashtable] # The parameters/configuration to use to run the task. $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Import-WhiskeyPowerShellModule -Name 'ProGetAutomation' $message = " Build: - PublishProGetAsset: CredentialID: ProGetCredential Path: - ""path/to/file.txt"" - ""path/to/anotherfile.txt"" Uri: http://proget.dev.webmd.com/ AssetPath: - ""path/to/exampleAsset"" - ""path/toanother/file.txt"" AssetDirectory: 'versions' " if( -not $TaskParameter['Path'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ("Please add a valid Path Parameter to your whiskey.yml file:" + $message) return } if( -not $TaskParameter['AssetDirectory'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ("Please add a valid Directory Parameter to your whiskey.yml file:" + $message) return } 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. Add the credential with the `Add-WhiskeyCredential` function:" + $message) return } $credential = Get-WhiskeyCredential -Context $TaskContext -ID $TaskParameter['CredentialID'] -PropertyName 'CredentialID' $session = New-ProGetSession -Uri $TaskParameter['Uri'] -Credential $credential foreach($path in $TaskParameter['Path']){ if( $TaskParameter['AssetPath'] -and @($TaskParameter['AssetPath']).count -eq @($TaskParameter['Path']).count){ $name = @($TaskParameter['AssetPath'])[$TaskParameter['Path'].indexOf($path)] } else { Stop-WhiskeyTask -TaskContext $TaskContext -Message ("There must be the same number of `Path` items as `AssetPath` Items. Each asset must have both a `Path` and an `AssetPath` in the whiskey.yml file." + $message) return } Set-ProGetAsset -Session $session -DirectoryName $TaskParameter['AssetDirectory'] -Path $name -FilePath $path } } function Publish-WhiskeyProGetUniversalPackage { [CmdletBinding()] [Whiskey.Task("PublishProGetUniversalPackage")] [Whiskey.RequiresTool('PowerShellModule::ProGetAutomation','ProGetAutomationPath',Version='0.9.*',VersionParameterName='ProGetAutomationVersion')] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Import-WhiskeyPowerShellModule -Name 'ProGetAutomation' $exampleTask = 'Publish: - PublishProGetUniversalPackage: CredentialID: ProGetCredential Uri: 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 $TaskParameter['Uri'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message "Uri is a mandatory property. It should be the URI to the ProGet instance where you want to publish your package: $exampleTask " 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 $TaskParameter['Uri'] -Credential $credential if( -not ($TaskParameter.ContainsKey('Path')) ) { $TaskParameter['Path'] = Join-Path -Path ($TaskContext.OutputDirectory | Split-Path -Leaf) -ChildPath '*.upack' } $errorActionParam = @{ } $allowMissingPackages = $false if( $TaskParameter.ContainsKey('AllowMissingPackage') ) { $allowMissingPackages = $TaskParameter['AllowMissingPackage'] | ConvertFrom-WhiskeyYamlScalar } if( $allowMissingPackages ) { $errorActionParam['ErrorAction'] = 'Ignore' } $packages = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' @errorActionParam | 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'] $taskPrefix = '[{0}] [{1}]' -f $session.Uri,$feedName $optionalParam = @{ } if( $TaskParameter['Timeout'] ) { $optionalParam['Timeout'] = $TaskParameter['Timeout'] } if( $TaskParameter['Overwrite'] ) { $optionalParam['Force'] = $TaskParameter['Overwrite'] | ConvertFrom-WhiskeyYamlScalar } Write-WhiskeyVerbose -Context $TaskContext -Message ('{0}' -f $taskPrefix) foreach( $package in $packages ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('{0} {1}' -f (' ' * $taskPrefix.Length),$package) Publish-ProGetUniversalPackage -Session $session -FeedName $feedName -PackagePath $package @optionalParam -ErrorAction Stop } } function Set-WhiskeyVariable { [CmdletBinding()] [Whiskey.Task("SetVariable",SupportsClean=$true,SupportsInitialize=$true)] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [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=$true,SupportsInitialize=$true)] param( [Parameter(Mandatory=$true)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory=$true)] [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()] [Whiskey.Task("Version")] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory)] [hashtable] $TaskParameter, [Whiskey.Tasks.ValidatePath(PathType='File')] [string] $Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState function ConvertTo-SemVer { param( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] $InputObject, $PropertyName, $VersionSource ) process { [SemVersion.SemanticVersion]$semver = $null if( -not [SemVersion.SemanticVersion]::TryParse($rawVersion,[ref]$semver) ) { if( $VersionSource ) { $VersionSource = ' ({0})' -f $VersionSource } $optionalParam = @{ } if( $PropertyName ) { $optionalParam['PropertyName'] = $PropertyName } Stop-WhiskeyTask -TaskContext $TaskContext -Message ('''{0}''{1} is not a semantic version. See http://semver.org for documentation on semantic versions.' -f $rawVersion,$VersionSource) @optionalParam } return $semver } } [Whiskey.BuildVersion]$buildVersion = $TaskContext.Version [SemVersion.SemanticVersion]$semver = $buildVersion.SemVer2 if( $TaskParameter[''] ) { $rawVersion = $TaskParameter[''] $semVer = $rawVersion | ConvertTo-SemVer -PropertyName 'Version' } elseif( $TaskParameter['Version'] ) { $rawVersion = $TaskParameter['Version'] $semVer = $rawVersion | ConvertTo-SemVer -PropertyName 'Version' } elseif( $Path ) { $fileInfo = Get-Item -Path $Path if( $fileInfo.Extension -eq '.psd1' ) { $rawVersion = Test-ModuleManifest -Path $Path -ErrorAction Ignore | Select-Object -ExpandProperty 'Version' if( -not $rawVersion ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to read version from PowerShell module manifest ''{0}'': the manifest is invalid or doesn''t contain a ''ModuleVersion'' property.' -f $Path) return } Write-WhiskeyVerbose -Context $TaskContext -Message ('Read version ''{0}'' from PowerShell module manifest ''{1}''.' -f $rawVersion,$Path) $semver = $rawVersion | ConvertTo-SemVer -VersionSource ('from PowerShell module manifest ''{0}''' -f $Path) } elseif( $fileInfo.Name -eq 'package.json' ) { try { $rawVersion = Get-Content -Path $Path -Raw | ConvertFrom-Json | Select-Object -ExpandProperty 'Version' -ErrorAction Ignore } catch { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Node package.json file ''{0}'' contains invalid JSON.' -f $Path) return } if( -not $rawVersion ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to read version from Node package.json ''{0}'': the ''Version'' property is missing.' -f $Path) return } Write-WhiskeyVerbose -Context $TaskContext -Message ('Read version ''{0}'' from Node package.json ''{1}''.' -f $rawVersion,$Path) $semVer = $rawVersion | ConvertTo-SemVer -VersionSource ('from Node package.json file ''{0}''' -f $Path) } elseif( $fileInfo.Extension -eq '.csproj' ) { [xml]$csprojXml = $null try { $csprojxml = Get-Content -Path $Path -Raw } catch { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('.NET .cspoj file ''{0}'' contains invalid XMl.' -f $Path) return } if( $csprojXml.DocumentElement.Attributes['xmlns'] ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('.NET .csproj file ''{0}'' 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.' -f $Path) return } $csprojVersionNode = $csprojXml.SelectSingleNode('/Project/PropertyGroup/Version') if( -not $csprojVersionNode ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Element ''/Project/PropertyGroup/Version'' does not exist in .NET .csproj file ''{0}''. Please create this element and set it to the MAJOR.MINOR.PATCH version of the next version of your assembly.' -f $Path) return } $rawVersion = $csprojVersionNode.InnerText Write-WhiskeyVerbose -Context $TaskContext -Message ('Read version ''{0}'' from .NET Core .csproj ''{1}''.' -f $rawVersion,$Path) $semver = $rawVersion | ConvertTo-SemVer -VersionSource ('from .NET .csproj file ''{0}''' -f $Path) } 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 ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to locate property "version ''x.x.x''" in metadata.rb file "{0}"' -f $Path ) return } Write-WhiskeyVerbose -Context $TaskContext -Message ('Read version "{0}" from metadata.rb file "{1}".' -f $rawVersion,$Path) $semver = $rawVersion | ConvertTo-SemVer -VersionSource ('from metadata.rb file "{0}"' -f $Path) } } $prerelease = $TaskParameter['Prerelease'] if( $prerelease -isnot [string] ) { $foundLabel = $false foreach( $object in $prerelease ) { foreach( $map in $object ) { if( -not ($map | Get-Member -Name 'Keys') ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Prerelease' -Message ('Unable to find keys in ''[{1}]{0}''. 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: {0} 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_BUILD_NUMBER) - develop: beta.$(WHISKEY_BUILD_NUMBER) ' -f $map,$map.GetType().FullName) return } foreach( $wildcardPattern in $map.Keys ) { if( $TaskContext.BuildMetadata.ScmBranch -like $wildcardPattern ) { Write-WhiskeyVerbose -Context $TaskContext -Message ('{0} -like {1}' -f $TaskContext.BuildMetadata.ScmBranch,$wildcardPattern) $foundLabel = $true $prerelease = $map[$wildcardPattern] break } else { Write-WhiskeyVerbose -Context $TaskContext -Message ('{0} -notlike {1}' -f $TaskContext.BuildMetadata.ScmBranch,$wildcardPattern) } } if( $foundLabel ) { break } } if( $foundLabel ) { break } } if( -not $foundLabel ) { $prerelease = '' } } if( $prerelease ) { $buildSuffix = '' if( $semver.Build ) { $buildSuffix = '+{0}' -f $semver.Build } $rawVersion = '{0}.{1}.{2}-{3}{4}' -f $semver.Major,$semver.Minor,$semver.Patch,$prerelease,$buildSuffix if( -not [SemVersion.SemanticVersion]::TryParse($rawVersion,[ref]$semver) ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Prerelease' -Message ('''{0}'' is not a valid prerelease version. Only letters, numbers, hyphens, and periods are allowed. See http://semver.org for full documentation.' -f $prerelease) return } } $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) ) { Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Build' -Message ('''{0}'' is not valid build metadata. Only letters, numbers, hyphens, and periods are allowed. See http://semver.org for full documentation.' -f $build) return } } # Build metadata is only available when running under a build server. if( $TaskContext.ByDeveloper ) { $semver = New-Object -TypeName 'SemVersion.SemanticVersion' $semver.Major,$semVer.Minor,$semVer.Patch,$semver.Prerelease } $buildVersion.SemVer2 = $semver Write-WhiskeyInfo -Context $TaskContext -Message ('Building version {0}' -f $semver) $buildVersion.Version = [version]('{0}.{1}.{2}' -f $semver.Major,$semver.Minor,$semver.Patch) Write-WhiskeyVerbose -Context $TaskContext -Message ('Version {0}' -f $buildVersion.Version) $buildVersion.SemVer2NoBuildMetadata = New-Object 'SemVersion.SemanticVersion' ($semver.Major,$semver.Minor,$semver.Patch,$semver.Prerelease) Write-WhiskeyVerbose -Context $TaskContext -Message ('SemVer2NoBuildMetadata {0}' -f $buildVersion.SemVer2NoBuildMetadata) $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 {0}' -f $buildVersion.SemVer1) } function New-WhiskeyZipArchive { [Whiskey.Task("Zip")] [Whiskey.RequiresTool('PowerShellModule::Zip','ZipPath',Version='0.3.*',VersionParameterName='ZipVersion')] [CmdletBinding()] param( [Parameter(Mandatory)] [Whiskey.Context] $TaskContext, [Parameter(Mandatory)] [hashtable] $TaskParameter ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Import-WhiskeyPowerShellModule -Name 'Zip' $archivePath = $TaskParameter['ArchivePath'] if( -not [IO.Path]::IsPathRooted($archivePath) ) { $archivePath = Join-Path -Path $TaskContext.BuildRoot -ChildPath $archivePath } $archivePath = Join-Path -Path $archivePath -ChildPath '.' # Get PowerShell to convert directory separator characters. $archivePath = [IO.Path]::GetFullPath($archivePath) $buildRootRegex = '^{0}(\\|/)' -f [regex]::Escape($TaskContext.BuildRoot) if( $archivePath -notmatch $buildRootRegex ) { Stop-WhiskeyTask -TaskContext $TaskContext -Message ('ArchivePath: path to ZIP archive "{0}" is outside the build root directory. Please change this path so it is under the "{1}" directory. We recommend using a relative path, as that it will always be resolved relative to the build root directory.' -f $archivePath,$TaskContext.BuildRoot) return } $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 } $parentPathParam = @{ } $sourceRoot = $TaskContext.BuildRoot if( $TaskParameter.ContainsKey('SourceRoot') ) { $sourceRoot = $TaskParameter['SourceRoot'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'SourceRoot' $parentPathParam['ParentPath'] = $sourceRoot } $sourceRootRegex = '^{0}' -f ([regex]::Escape($sourceRoot)) Write-WhiskeyInfo -Context $TaskContext -Message ('Creating ZIP archive "{0}".' -f ($archivePath -replace $sourceRootRegex,'').Trim('\','/')) $archiveDirectory = $archivePath | Split-Path -Parent if( -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 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' @parentPathParam if( -not $sourcePaths ) { return } foreach( $sourcePath in $sourcePaths ) { $relativePath = $sourcePath -replace $sourceRootRegex,'' $relativePath = $relativePath.Trim([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) $addParams = @{ BasePath = $sourceRoot } $overrideInfo = '' if( $override ) { $addParams = @{ PackageItemName = $destinationItemName } $overrideInfo = ' -> {0}' -f $destinationItemName } if( (Test-Path -Path $sourcePath -PathType Leaf) ) { Write-WhiskeyInfo -Context $TaskContext -Message (' compressing file {0}{1}' -f $relativePath,$overrideInfo) 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 ) { $addParams['BasePath'] = $sourcePath $addParams['EntryParentPath'] = $destinationItemName $addParams.Remove('PackageItemName') $overrideInfo = ' -> {0}' -f $destinationItemName } $typeDesc = 'directory ' if( $TaskParameter['Include'] -or $TaskParameter['Exclude'] ) { $typeDesc = 'filtered directory' } Write-WhiskeyInfo -Context $TaskContext -Message (' compressing {0} {1}{2}' -f $typeDesc,$relativePath,$overrideInfo) Find-Item -Path $sourcePath | Add-ZipArchiveEntry -ZipArchivePath $archivePath @addParams @behaviorParams } } } |