diff --git a/.gitignore b/.gitignore index ead32ef..bd9823a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ localrun.rvo.ps1 +# Visual Studio 2015/2017/2019 cache/options directory +.vs/ diff --git a/extension/images/Screenshots/task-config-name-version.png b/extension/images/Screenshots/task-config-name-version.png new file mode 100644 index 0000000..70fda20 Binary files /dev/null and b/extension/images/Screenshots/task-config-name-version.png differ diff --git a/extension/images/Screenshots/task-config-packages.png b/extension/images/Screenshots/task-config-packages.png new file mode 100644 index 0000000..6a94ed2 Binary files /dev/null and b/extension/images/Screenshots/task-config-packages.png differ diff --git a/extension/images/Screenshots/task-config.png b/extension/images/Screenshots/task-config.png deleted file mode 100644 index 0b3be74..0000000 Binary files a/extension/images/Screenshots/task-config.png and /dev/null differ diff --git a/localrun.ps1 b/localrun.ps1 index 7fa3477..1d7494e 100644 --- a/localrun.ps1 +++ b/localrun.ps1 @@ -1,21 +1,32 @@ -. .\vsts-promotepackage-task\vsts-promotepackage-task.ps1 -localRun $true +Import-Module $PSScriptRoot\vsts-promotepackage-task\ps_modules\VsTsTaskSdk #get these values, by running the task in the pipeline with system.debug true -$feedName = "Guid of Feed" -$packageId = "Guid of package" -$packageVersion = "version of package" -$releaseView = "Guid of release view" +$env:INPUT_FEED = "Guid or name of Feed" +$env:INPUT_INPUTTYPE = "nameVersion" # nameVersion or packageFiles + +# nameVersion inputs: +$env:INPUT_PACKAGEIDS = "Guids or names of package(s)" # comma or semicolon separated +$env:INPUT_VERSION = "version of package" + +# packageFiles inputs: +$env:INPUT_PACKAGESDIRECTORY = "Root directory of package files" +$env:INPUT_PACKAGESPATTERN = "**\*.nupkg`n!**\*.symbols.nupkg`n**\*.tgz" # newline or semicolon separated + +$env:INPUT_RELEASEVIEW = "Guid or name of release view" + +# Info required to make the task run in test mode +$env:PROMOTEPACKAGE_PAT = "pat" +$azdoaccount = "Azure DevOps account name" $connectedServiceName="localrun" -$env:pat="pat" Describe 'Test Promote Release View' { It 'Uses a Azure DevOps Account' { $env:SYSTEM_TEAMFOUNDATIONSERVERURI="https://dev.azure.com/$($azdoaccount)/" - Run + Invoke-VsTsTaskScript -ScriptBlock { . $PSScriptRoot\vsts-promotepackage-task\vsts-promotepackage-task.ps1 } -Verbose } It 'Uses a Visual Studio Account' { $env:SYSTEM_TEAMFOUNDATIONSERVERURI="https://$($azdoaccount).visualstudio.com/" - Run + Invoke-VsTsTaskScript -ScriptBlock { . $PSScriptRoot\vsts-promotepackage-task\vsts-promotepackage-task.ps1 } -Verbose } } \ No newline at end of file diff --git a/testregex.ps1 b/testregex.ps1 index d8f8e60..76ccf60 100644 --- a/testregex.ps1 +++ b/testregex.ps1 @@ -4,24 +4,18 @@ function Get-Account() { $url ) - if ($url -like "https://vsrm.dev.azure.com*") { + $uri = [uri]$url + $hostName = $uri.Host + + if (($hostName -eq "dev.azure.com") -or ($hostName -eq "vsrm.dev.azure.com")) { #new style - $account = ($env:SYSTEM_TEAMFOUNDATIONSERVERURI -replace "https://vsrm.dev.azure.com/(.*)(\/)", '$1').split('.')[0] - return $account - } - elseif ($url -like "https://dev.azure.com*") - { - #new style - $account = ($env:SYSTEM_TEAMFOUNDATIONSERVERURI -replace "https://dev.azure.com/(.*)(\/)", '$1').split('.')[0] - return $account - } - elseif ($url -like "*visualstudio.com*") - { + $account = $uri.Segments[1].TrimEnd('/') # First segment after hostname + return $account + } elseif ($hostName.EndsWith("visualstudio.com")) { #old style - $account = ($env:SYSTEM_TEAMFOUNDATIONSERVERURI -replace "https://(.*)\.visualstudio\.com/", '$1').split('.')[0] + $account = $hostName.Split('.')[0] # First subdomain of hostname return $account - } - else { + } else { Write-Host "On-Premise TFS / Azure DevOps Server not supported" } } diff --git a/vss-extension.json b/vss-extension.json index ce7937e..3d2d429 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -14,7 +14,7 @@ ], "description": "This task promotes a package to a Release View in Azure DevOps Artifacts", "categories": [ - "Build and release" + "Azure Pipelines" ], "tags": [ "Extension", @@ -28,7 +28,10 @@ "path": "extension/Images/Screenshots/add-task.png" }, { - "path": "extension/Images/Screenshots/task-config.png" + "path": "extension/Images/Screenshots/task-config-name-version.png" + }, + { + "path": "extension/Images/Screenshots/task-config-packages.png" } ], "content": { diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/FindFunctions.ps1 b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/FindFunctions.ps1 index d2087b1..8687f1b 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/FindFunctions.ps1 +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/FindFunctions.ps1 @@ -297,6 +297,9 @@ Array of paths. .PARAMETER Pattern Patterns to apply. Supports interleaved exclude patterns. +.PARAMETER PatternRoot +Default root to apply to unrooted patterns. Not applied to basename-only patterns when Options.MatchBase is true. + .PARAMETER Options When the Options parameter is not specified, defaults to (New-VstsMatchOptions -Dot -NoBrace -NoCase). #> @@ -307,6 +310,8 @@ function Select-Match { [string[]]$ItemPath, [Parameter()] [string[]]$Pattern, + [Parameter()] + [string]$PatternRoot, $Options) @@ -344,7 +349,7 @@ function Select-Match { continue } - # Set NoComment. + # Set NoComment. Brace expansion could result in a leading '#'. $Options.NoComment = $true # Determine whether pattern is include or exclude. @@ -368,46 +373,80 @@ function Select-Match { $Options.NoNegate = $true $Options.FlipNegate = $false - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue + # Expand braces - required to accurately root patterns. + $expanded = $null + $preExpanded = $pat + if ($Options.NoBrace) { + $expanded = @( $pat ) + } else { + # Convert slashes on Windows before calling braceExpand(). Unfortunately this means braces cannot + # be escaped on Windows, this limitation is consistent with current limitations of minimatch (3.0.3). + Write-Verbose "Expanding braces." + $convertedPattern = $pat -replace '\\', '/' + $expanded = [Minimatch.Minimatcher]::BraceExpand( + $convertedPattern, + (ConvertTo-MinimatchOptions -Options $Options)) } - if ($isIncludePattern) { - # Apply the pattern. - Write-Verbose 'Applying include pattern against original list' - $matchResults = [Minimatch.Minimatcher]::Filter( - $ItemPath, - $pat, - (ConvertTo-MinimatchOptions -Options $Options)) + # Set NoBrace. + $Options.NoBrace = $true - # Union the results. - $matchCount = 0 - foreach ($matchResult in $matchResults) { - $matchCount++ - $map[$matchResult] = $true + foreach ($pat in $expanded) { + if ($pat -ne $preExpanded) { + Write-Verbose "Pattern: '$pat'" } - Write-Verbose "$matchCount matches" - } - else { - # Apply the pattern. - Write-Verbose 'Applying exclude pattern against original list' - $matchResults = [Minimatch.Minimatcher]::Filter( - $ItemPath, - $pat, - (ConvertTo-MinimatchOptions -Options $Options)) + # Trim and skip empty. + $pat = "$pat".Trim() + if (!$pat) { + Write-Verbose "Skipping empty pattern." + continue + } + + # Root the pattern when all of the following conditions are true: + if ($PatternRoot -and # PatternRoot is supplied + !(Test-Rooted -Path $pat) -and # AND pattern is not rooted + # # AND MatchBase=false or not basename only + (!$Options.MatchBase -or ($pat -replace '\\', '/').IndexOf('/') -ge 0)) { - # Subtract the results. - $matchCount = 0 - foreach ($matchResult in $matchResults) { - $matchCount++ - $map.Remove($matchResult) + # Root the include pattern. + $pat = Get-RootedPattern -DefaultRoot $PatternRoot -Pattern $pat + Write-Verbose "After Get-RootedPattern, pattern: '$pat'" } - Write-Verbose "$matchCount matches" + if ($isIncludePattern) { + # Apply the pattern. + Write-Verbose 'Applying include pattern against original list.' + $matchResults = [Minimatch.Minimatcher]::Filter( + $ItemPath, + $pat, + (ConvertTo-MinimatchOptions -Options $Options)) + + # Union the results. + $matchCount = 0 + foreach ($matchResult in $matchResults) { + $matchCount++ + $map[$matchResult] = $true + } + + Write-Verbose "$matchCount matches" + } else { + # Apply the pattern. + Write-Verbose 'Applying exclude pattern against original list' + $matchResults = [Minimatch.Minimatcher]::Filter( + $ItemPath, + $pat, + (ConvertTo-MinimatchOptions -Options $Options)) + + # Subtract the results. + $matchCount = 0 + foreach ($matchResult in $matchResults) { + $matchCount++ + $map.Remove($matchResult) + } + + Write-Verbose "$matchCount matches" + } } } diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/InputFunctions.ps1 b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/InputFunctions.ps1 index 21c4ade..d7eb7c2 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/InputFunctions.ps1 +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/InputFunctions.ps1 @@ -64,6 +64,74 @@ function Get-Endpoint { } } +<# +.SYNOPSIS +Gets a secure file ticket. + +.DESCRIPTION +Gets the secure file ticket that can be used to download the secure file contents. + +.PARAMETER Id +Secure file id. + +.PARAMETER Require +Writes an error to the error pipeline if the ticket is not found. +#> +function Get-SecureFileTicket { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Id, + [switch]$Require) + + $originalErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = 'Stop' + + $description = Get-LocString -Key PSLIB_Input0 -ArgumentList $Id + $key = "SECUREFILE_TICKET_$Id" + + Get-VaultValue -Description $description -Key $key -Require:$Require + } catch { + $ErrorActionPreference = $originalErrorActionPreference + Write-Error $_ + } +} + +<# +.SYNOPSIS +Gets a secure file name. + +.DESCRIPTION +Gets the name for a secure file. + +.PARAMETER Id +Secure file id. + +.PARAMETER Require +Writes an error to the error pipeline if the ticket is not found. +#> +function Get-SecureFileName { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Id, + [switch]$Require) + + $originalErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = 'Stop' + + $description = Get-LocString -Key PSLIB_Input0 -ArgumentList $Id + $key = "SECUREFILE_NAME_$Id" + + Get-VaultValue -Description $description -Key $key -Require:$Require + } catch { + $ErrorActionPreference = $originalErrorActionPreference + Write-Error $_ + } +} + <# .SYNOPSIS Gets an input. diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 index a8462ef..a2eeb9d 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 @@ -3,8 +3,8 @@ $script:loggingCommandEscapeMappings = @( # TODO: WHAT ABOUT "="? WHAT ABOUT "%" New-Object psobject -Property @{ Token = ';' ; Replacement = '%3B' } New-Object psobject -Property @{ Token = "`r" ; Replacement = '%0D' } New-Object psobject -Property @{ Token = "`n" ; Replacement = '%0A' } + New-Object psobject -Property @{ Token = "]" ; Replacement = '%5D' } ) -# TODO: BUG: Escape ] # TODO: BUG: Escape % ??? # TODO: Add test to verify don't need to escape "=". @@ -36,6 +36,50 @@ function Write-AddAttachment { .SYNOPSIS See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md +.PARAMETER AsOutput +Indicates whether to write the logging command directly to the host or to the output pipeline. +#> +function Write-UploadSummary { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Path, + [switch]$AsOutput) + + Write-LoggingCommand -Area 'task' -Event 'uploadsummary' -Data $Path -AsOutput:$AsOutput +} + +<# +.SYNOPSIS +See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md + +.PARAMETER AsOutput +Indicates whether to write the logging command directly to the host or to the output pipeline. +#> +function Write-SetEndpoint { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Id, + [Parameter(Mandatory = $true)] + [string]$Field, + [Parameter(Mandatory = $true)] + [string]$Key, + [Parameter(Mandatory = $true)] + [string]$Value, + [switch]$AsOutput) + + Write-LoggingCommand -Area 'task' -Event 'setendpoint' -Data $Value -Properties @{ + 'id' = $Id + 'field' = $Field + 'key' = $Key + } -AsOutput:$AsOutput +} + +<# +.SYNOPSIS +See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md + .PARAMETER AsOutput Indicates whether to write the logging command directly to the host or to the output pipeline. #> @@ -287,6 +331,40 @@ function Write-TaskWarning { .SYNOPSIS See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md +.PARAMETER AsOutput +Indicates whether to write the logging command directly to the host or to the output pipeline. +#> +function Write-UploadFile { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Path, + [switch]$AsOutput) + + Write-LoggingCommand -Area 'task' -Event 'uploadfile' -Data $Path -AsOutput:$AsOutput +} + +<# +.SYNOPSIS +See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md + +.PARAMETER AsOutput +Indicates whether to write the logging command directly to the host or to the output pipeline. +#> +function Write-PrependPath { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Path, + [switch]$AsOutput) + + Write-LoggingCommand -Area 'task' -Event 'prependpath' -Data $Path -AsOutput:$AsOutput +} + +<# +.SYNOPSIS +See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md + .PARAMETER AsOutput Indicates whether to write the logging command directly to the host or to the output pipeline. #> @@ -341,6 +419,23 @@ function Write-UploadBuildLog { Write-LoggingCommand -Area 'build' -Event 'uploadlog' -Data $Path -AsOutput:$AsOutput } +<# +.SYNOPSIS +See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md + +.PARAMETER AsOutput +Indicates whether to write the logging command directly to the host or to the output pipeline. +#> +function Write-UpdateReleaseName { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Name, + [switch]$AsOutput) + + Write-LoggingCommand -Area 'release' -Event 'updatereleasename' -Data $Name -AsOutput:$AsOutput +} + ######################################## # Private functions. ######################################## diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 index f81e377..51cda34 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 @@ -202,167 +202,4 @@ function Get-FullNormalizedPath { } $outPath -} - -######################################## -# Types. -######################################## -# If the type has already been loaded once, then it is not loaded again. -Write-Verbose "Adding long path native methods." -Add-Type -Debug:$false -TypeDefinition @' -namespace VstsTaskSdk.FS -{ - using System; - using System.Runtime.InteropServices; - - public static class NativeMethods - { - private const string Kernel32Dll = "kernel32.dll"; - - [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool FindClose(IntPtr hFindFile); - - // HANDLE WINAPI FindFirstFile( - // _In_ LPCTSTR lpFileName, - // _Out_ LPWIN32_FIND_DATA lpFindFileData - // ); - [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] - public static extern SafeFindHandle FindFirstFile( - [MarshalAs(UnmanagedType.LPTStr)] - string fileName, - [In, Out] FindData findFileData - ); - - //HANDLE WINAPI FindFirstFileEx( - // _In_ LPCTSTR lpFileName, - // _In_ FINDEX_INFO_LEVELS fInfoLevelId, - // _Out_ LPVOID lpFindFileData, - // _In_ FINDEX_SEARCH_OPS fSearchOp, - // _Reserved_ LPVOID lpSearchFilter, - // _In_ DWORD dwAdditionalFlags - //); - [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] - public static extern SafeFindHandle FindFirstFileEx( - [MarshalAs(UnmanagedType.LPTStr)] - string fileName, - [In] FindInfoLevel fInfoLevelId, - [In, Out] FindData lpFindFileData, - [In] FindSearchOps fSearchOp, - IntPtr lpSearchFilter, - [In] FindFlags dwAdditionalFlags - ); - - [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool FindNextFile(SafeFindHandle hFindFile, [In, Out] FindData lpFindFileData); - - [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] - public static extern int GetFileAttributes(string lpFileName); - - [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] - public static extern uint GetFullPathName( - [MarshalAs(UnmanagedType.LPTStr)] - string lpFileName, - uint nBufferLength, - [Out] - System.Text.StringBuilder lpBuffer, - System.Text.StringBuilder lpFilePart - ); - } - - //for mapping to the WIN32_FIND_DATA native structure - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public sealed class FindData - { - // NOTE: - // Although it may seem correct to Marshal the string members of this class as UnmanagedType.LPWStr, they - // must explicitly remain UnmanagedType.ByValTStr with the size constraints noted. Otherwise we end up with - // COM Interop exceptions while trying to marshal the data across the PInvoke boundaries. - public int fileAttributes; - public System.Runtime.InteropServices.ComTypes.FILETIME creationTime; - public System.Runtime.InteropServices.ComTypes.FILETIME lastAccessTime; - public System.Runtime.InteropServices.ComTypes.FILETIME lastWriteTime; - public int nFileSizeHigh; - public int nFileSizeLow; - public int dwReserved0; - public int dwReserved1; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] - public string fileName; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] - public string alternateFileName; - } - - //A Win32 safe find handle in which a return value of -1 indicates it's invalid - public sealed class SafeFindHandle : Microsoft.Win32.SafeHandles.SafeHandleMinusOneIsInvalid - { - public SafeFindHandle() - : base(true) - { - return; - } - - [System.Runtime.ConstrainedExecution.ReliabilityContract(System.Runtime.ConstrainedExecution.Consistency.WillNotCorruptState, System.Runtime.ConstrainedExecution.Cer.Success)] - protected override bool ReleaseHandle() - { - return NativeMethods.FindClose(handle); - } - } - - // Refer https://msdn.microsoft.com/en-us/library/windows/desktop/gg258117(v=vs.85).aspx - [Flags] - public enum Attributes : uint - { - None = 0x00000000, - Readonly = 0x00000001, - Hidden = 0x00000002, - System = 0x00000004, - Directory = 0x00000010, - Archive = 0x00000020, - Device = 0x00000040, - Normal = 0x00000080, - Temporary = 0x00000100, - SparseFile = 0x00000200, - ReparsePoint = 0x00000400, - Compressed = 0x00000800, - Offline = 0x00001000, - NotContentIndexed = 0x00002000, - Encrypted = 0x00004000, - IntegrityStream = 0x00008000, - Virtual = 0x00010000, - NoScrubData = 0x00020000, - Write_Through = 0x80000000, - Overlapped = 0x40000000, - NoBuffering = 0x20000000, - RandomAccess = 0x10000000, - SequentialScan = 0x08000000, - DeleteOnClose = 0x04000000, - BackupSemantics = 0x02000000, - PosixSemantics = 0x01000000, - OpenReparsePoint = 0x00200000, - OpenNoRecall = 0x00100000, - FirstPipeInstance = 0x00080000 - } - - [Flags] - public enum FindFlags - { - None = 0, - CaseSensitive = 1, - LargeFetch = 2, - } - - public enum FindInfoLevel - { - Standard = 0, - Basic = 1, - } - - public enum FindSearchOps - { - NameMatch = 0, - LimitToDirectories = 1, - LimitToDevices = 2, - } -} -'@ +} \ No newline at end of file diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Minimatch.dll b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Minimatch.dll new file mode 100644 index 0000000..700ddc4 Binary files /dev/null and b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Minimatch.dll differ diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml new file mode 100644 index 0000000..028eaf0 Binary files /dev/null and b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml differ diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 index 94832ae..9d43b20 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 @@ -3,7 +3,7 @@ Gets assembly reference information. .DESCRIPTION -Not supported for use during task exection. This function is only intended to help developers resolve the minimal set of DLLs that need to be bundled when consuming the VSTS REST SDK or TFS Extended Client SDK. The interface and output may change between patch releases of the VSTS Task SDK. +Not supported for use during task execution. This function is only intended to help developers resolve the minimal set of DLLs that need to be bundled when consuming the VSTS REST SDK or TFS Extended Client SDK. The interface and output may change between patch releases of the VSTS Task SDK. Only a subset of the referenced assemblies may actually be required, depending on the functionality used by your task. It is best to bundle only the DLLs required for your scenario. @@ -24,7 +24,7 @@ function Get-AssemblyReference { [string]$LiteralPath) $ErrorActionPreference = 'Stop' - Write-Warning "Not supported for use during task exection. This function is only intended to help developers resolve the minimal set of DLLs that need to be bundled when consuming the VSTS REST SDK or TFS Extended Client SDK. The interface and output may change between patch releases of the VSTS Task SDK." + Write-Warning "Not supported for use during task execution. This function is only intended to help developers resolve the minimal set of DLLs that need to be bundled when consuming the VSTS REST SDK or TFS Extended Client SDK. The interface and output may change between patch releases of the VSTS Task SDK." Write-Output '' Write-Warning "Only a subset of the referenced assemblies may actually be required, depending on the functionality used by your task. It is best to bundle only the DLLs required for your scenario." $directory = [System.IO.Path]::GetDirectoryName($LiteralPath) @@ -178,7 +178,7 @@ If not specified, defaults to the directory of the entry script for the task. URI to use when initializing the service. If not specified, defaults to System.TeamFoundationCollectionUri. .PARAMETER TfsClientCredentials -Credentials to use when intializing the service. If not specified, the default uses the agent job token to construct the credentials object. The identity associated with the token depends on the scope selected in the build/release definition (either the project collection build/release service identity, or the project build/release service identity). +Credentials to use when initializing the service. If not specified, the default uses the agent job token to construct the credentials object. The identity associated with the token depends on the scope selected in the build/release definition (either the project collection build/release service identity, or the project build/release service identity). .EXAMPLE $versionControlServer = Get-VstsTfsService -TypeName Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer @@ -322,7 +322,16 @@ If not specified, defaults to the directory of the entry script for the task. # URI to use when initializing the HTTP client. If not specified, defaults to System.TeamFoundationCollectionUri. # .PARAMETER VssCredentials -# Credentials to use when intializing the HTTP client. If not specified, the default uses the agent job token to construct the credentials object. The identity associated with the token depends on the scope selected in the build/release definition (either the project collection build/release service identity, or the project build/release service identity). +# Credentials to use when initializing the HTTP client. If not specified, the default uses the agent job token to construct the credentials object. The identity associated with the token depends on the scope selected in the build/release definition (either the project collection build/release service identity, or the project build/release service identity). + +# .PARAMETER WebProxy +# WebProxy to use when initializing the HTTP client. If not specified, the default uses the proxy configuration agent current has. + +# .PARAMETER ClientCert +# ClientCert to use when initializing the HTTP client. If not specified, the default uses the client certificate agent current has. + +# .PARAMETER IgnoreSslError +# Skip SSL server certificate validation on all requests made by this HTTP client. If not specified, the default is to validate SSL server certificate. .EXAMPLE $projectHttpClient = Get-VstsVssHttpClient -TypeName Microsoft.TeamFoundation.Core.WebApi.ProjectHttpClient @@ -338,7 +347,13 @@ function Get-VssHttpClient { [string]$Uri, - $VssCredentials) + $VssCredentials, + + $WebProxy = (Get-WebProxy), + + $ClientCert = (Get-ClientCertificate), + + [switch]$IgnoreSslError) Trace-EnteringInvocation -InvocationInfo $MyInvocation $originalErrorActionPreference = $ErrorActionPreference @@ -361,10 +376,38 @@ function Get-VssHttpClient { # Validate the type can be loaded. $null = Get-OMType -TypeName $TypeName -OMKind 'WebApi' -OMDirectory $OMDirectory -Require + # Update proxy setting for vss http client + [Microsoft.VisualStudio.Services.Common.VssHttpMessageHandler]::DefaultWebProxy = $WebProxy + + # Update client certificate setting for vss http client + $null = Get-OMType -TypeName 'Microsoft.VisualStudio.Services.Common.VssHttpRequestSettings' -OMKind 'WebApi' -OMDirectory $OMDirectory -Require + $null = Get-OMType -TypeName 'Microsoft.VisualStudio.Services.WebApi.VssClientHttpRequestSettings' -OMKind 'WebApi' -OMDirectory $OMDirectory -Require + [Microsoft.VisualStudio.Services.Common.VssHttpRequestSettings]$Settings = [Microsoft.VisualStudio.Services.WebApi.VssClientHttpRequestSettings]::Default.Clone() + + if ($ClientCert) { + $null = Get-OMType -TypeName 'Microsoft.VisualStudio.Services.WebApi.VssClientCertificateManager' -OMKind 'WebApi' -OMDirectory $OMDirectory -Require + $null = [Microsoft.VisualStudio.Services.WebApi.VssClientCertificateManager]::Instance.ClientCertificates.Add($ClientCert) + + $Settings.ClientCertificateManager = [Microsoft.VisualStudio.Services.WebApi.VssClientCertificateManager]::Instance + } + + # Skip SSL server certificate validation + [bool]$SkipCertValidation = (Get-TaskVariable -Name Agent.SkipCertValidation -AsBool) -or $IgnoreSslError + if ($SkipCertValidation) { + if ($Settings.GetType().GetProperty('ServerCertificateValidationCallback')) { + Write-Verbose "Ignore any SSL server certificate validation errors."; + $Settings.ServerCertificateValidationCallback = [VstsTaskSdk.VstsHttpHandlerSettings]::UnsafeSkipServerCertificateValidation + } + else { + # OMDirectory has older version of Microsoft.VisualStudio.Services.Common.dll + Write-Verbose "The version of 'Microsoft.VisualStudio.Services.Common.dll' does not support skip SSL server certificate validation." + } + } + # Try to construct the HTTP client. Write-Verbose "Constructing HTTP client." try { - return New-Object $TypeName($Uri, $VssCredentials) + return New-Object $TypeName($Uri, $VssCredentials, $Settings) } catch { # Rethrow if the exception is not due to Newtonsoft.Json DLL not found. if ($_.Exception.InnerException -isnot [System.IO.FileNotFoundException] -or @@ -391,7 +434,7 @@ function Get-VssHttpClient { # dependency on the 6.0.0.0 Newtonsoft.Json DLL, while other parts reference # the 8.0.0.0 Newtonsoft.Json DLL. Write-Verbose "Adding assembly resolver." - $onAssemblyResolve = [System.ResolveEventHandler]{ + $onAssemblyResolve = [System.ResolveEventHandler] { param($sender, $e) if ($e.Name -like 'Newtonsoft.Json, *') { @@ -406,7 +449,7 @@ function Get-VssHttpClient { try { # Try again to construct the HTTP client. Write-Verbose "Trying again to construct the HTTP client." - return New-Object $TypeName($Uri, $VssCredentials) + return New-Object $TypeName($Uri, $VssCredentials, $Settings) } finally { # Unregister the assembly resolver. Write-Verbose "Removing assemlby resolver." @@ -421,6 +464,73 @@ function Get-VssHttpClient { } } +<# +.SYNOPSIS +Gets a VstsTaskSdk.VstsWebProxy + +.DESCRIPTION +Gets an instance of a VstsTaskSdk.VstsWebProxy that has same proxy configuration as Build/Release agent. + +VstsTaskSdk.VstsWebProxy implement System.Net.IWebProxy interface. + +.EXAMPLE +$webProxy = Get-VstsWebProxy +$webProxy.GetProxy(New-Object System.Uri("https://github.com/Microsoft/vsts-task-lib")) +#> +function Get-WebProxy { + [CmdletBinding()] + param() + + Trace-EnteringInvocation -InvocationInfo $MyInvocation + try { + # Min agent version that supports proxy + Assert-Agent -Minimum '2.105.7' + + $proxyUrl = Get-TaskVariable -Name Agent.ProxyUrl + $proxyUserName = Get-TaskVariable -Name Agent.ProxyUserName + $proxyPassword = Get-TaskVariable -Name Agent.ProxyPassword + $proxyBypassListJson = Get-TaskVariable -Name Agent.ProxyBypassList + [string[]]$ProxyBypassList = ConvertFrom-Json -InputObject $ProxyBypassListJson + + return New-Object -TypeName VstsTaskSdk.VstsWebProxy -ArgumentList @($proxyUrl, $proxyUserName, $proxyPassword, $proxyBypassList) + } + finally { + Trace-LeavingInvocation -InvocationInfo $MyInvocation + } +} + +<# +.SYNOPSIS +Gets a client certificate for current connected TFS instance + +.DESCRIPTION +Gets an instance of a X509Certificate2 that is the client certificate Build/Release agent used. + +.EXAMPLE +$x509cert = Get-ClientCertificate +WebRequestHandler.ClientCertificates.Add(x509cert) +#> +function Get-ClientCertificate { + [CmdletBinding()] + param() + + Trace-EnteringInvocation -InvocationInfo $MyInvocation + try { + # Min agent version that supports client certificate + Assert-Agent -Minimum '2.122.0' + + [string]$clientCert = Get-TaskVariable -Name Agent.ClientCertArchive + [string]$clientCertPassword = Get-TaskVariable -Name Agent.ClientCertPassword + + if ($clientCert -and (Test-Path -LiteralPath $clientCert -PathType Leaf)) { + return New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($clientCert, $clientCertPassword) + } + } + finally { + Trace-LeavingInvocation -InvocationInfo $MyInvocation + } +} + ######################################## # Private functions. ######################################## diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-de/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-de/resources.resjson index ec97e9b..248b674 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-de/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-de/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "Agentversion {0} oder höher ist erforderlich.", "loc.messages.PSLIB_ContainerPathNotFound0": "Der Containerpfad wurde nicht gefunden: \"{0}\".", "loc.messages.PSLIB_EndpointAuth0": "\"{0}\"-Dienstendpunkt-Anmeldeinformationen", "loc.messages.PSLIB_EndpointUrl0": "\"{0}\"-Dienstendpunkt-URL", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson index c6c6419..66c17bc 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "Agent version {0} or higher is required.", "loc.messages.PSLIB_ContainerPathNotFound0": "Container path not found: '{0}'", "loc.messages.PSLIB_EndpointAuth0": "'{0}' service endpoint credentials", "loc.messages.PSLIB_EndpointUrl0": "'{0}' service endpoint URL", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-es/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-es/resources.resjson index d09d96c..b79ac21 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-es/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-es/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "Se require la versión {0} o posterior del agente.", "loc.messages.PSLIB_ContainerPathNotFound0": "No se encuentra la ruta de acceso del contenedor: '{0}'", "loc.messages.PSLIB_EndpointAuth0": "Credenciales del punto de conexión de servicio '{0}'", "loc.messages.PSLIB_EndpointUrl0": "URL del punto de conexión de servicio '{0}'", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-fr/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-fr/resources.resjson index 6606ddd..dc2da05 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-fr/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-fr/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "L'agent version {0} (ou une version ultérieure) est obligatoire.", "loc.messages.PSLIB_ContainerPathNotFound0": "Le chemin du conteneur est introuvable : '{0}'", "loc.messages.PSLIB_EndpointAuth0": "Informations d'identification du point de terminaison de service '{0}'", "loc.messages.PSLIB_EndpointUrl0": "URL du point de terminaison de service '{0}'", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson index 0b54d99..77bea53 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "È richiesta la versione dell'agente {0} o superiore.", "loc.messages.PSLIB_ContainerPathNotFound0": "Percorso del contenitore non trovato: '{0}'", "loc.messages.PSLIB_EndpointAuth0": "Credenziali dell'endpoint servizio '{0}'", "loc.messages.PSLIB_EndpointUrl0": "URL dell'endpoint servizio '{0}'", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-jp/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-jp/resources.resjson index b0e2bdf..9f2f9fe 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-jp/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-jp/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "バージョン {0} 以降のエージェントが必要です。", "loc.messages.PSLIB_ContainerPathNotFound0": "コンテナーのパスが見つかりません: '{0}'", "loc.messages.PSLIB_EndpointAuth0": "'{0}' サービス エンドポイントの資格情報", "loc.messages.PSLIB_EndpointUrl0": "'{0}' サービス エンドポイントの URL", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson index 60df35b..17e0d28 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "에이전트 버전 {0} 이상이 필요합니다.", "loc.messages.PSLIB_ContainerPathNotFound0": "컨테이너 경로를 찾을 수 없음: '{0}'", "loc.messages.PSLIB_EndpointAuth0": "'{0}' 서비스 끝점 자격 증명", "loc.messages.PSLIB_EndpointUrl0": "'{0}' 서비스 끝점 URL", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson index 2d42208..b9c5a27 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "Требуется версия агента {0} или более поздняя.", "loc.messages.PSLIB_ContainerPathNotFound0": "Путь к контейнеру не найден: \"{0}\".", "loc.messages.PSLIB_EndpointAuth0": "Учетные данные конечной точки службы \"{0}\"", "loc.messages.PSLIB_EndpointUrl0": "URL-адрес конечной точки службы \"{0}\"", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson index 80adce2..49d824b 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "需要代理版本 {0} 或更高版本。", "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路径:“{0}”", "loc.messages.PSLIB_EndpointAuth0": "“{0}”服务终结点凭据", "loc.messages.PSLIB_EndpointUrl0": "“{0}”服务终结点 URL", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson index d6a041c..7cbf22e 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson @@ -1,4 +1,5 @@ { + "loc.messages.PSLIB_AgentVersion0Required": "需要代理程式版本 {0} 或更新的版本。", "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路徑: '{0}'", "loc.messages.PSLIB_EndpointAuth0": "'{0}' 服務端點認證", "loc.messages.PSLIB_EndpointUrl0": "'{0}' 服務端點 URL", diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/ToolFunctions.ps1 b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/ToolFunctions.ps1 index 0d0f444..7e62ebd 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/ToolFunctions.ps1 +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/ToolFunctions.ps1 @@ -1,3 +1,27 @@ +<# +.SYNOPSIS +Asserts the agent version is at least the specified minimum. + +.PARAMETER Minimum +Minimum version - must be 2.104.1 or higher. +#> +function Assert-Agent { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [version]$Minimum) + + if (([version]'2.104.1').CompareTo($Minimum) -ge 1) { + Write-Error "Assert-Agent requires the parameter to be 2.104.1 or higher." + return + } + + $agent = Get-TaskVariable -Name 'agent.version' + if (!$agent -or $Minimum.CompareTo([version]$agent) -ge 1) { + Write-Error (Get-LocString -Key 'PSLIB_AgentVersion0Required' -ArgumentList $Minimum) + } +} + <# .SYNOPSIS Asserts that a path exists. Throws if the path does not exist. diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.dll b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.dll new file mode 100644 index 0000000..54938ab Binary files /dev/null and b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.dll differ diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 index 7229eb9..483585d 100644 Binary files a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 and b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 differ diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 index 90b1c72..43b9561 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 @@ -12,6 +12,13 @@ if ($host.Name -ne 'ConsoleHost') { [bool]$script:nonInteractive = "$($ModuleParameters['NonInteractive'])" -eq 'true' Write-Verbose "NonInteractive: $script:nonInteractive" +# VstsTaskSdk.dll contains the TerminationException and NativeMethods for handle long path +# We used to do inline C# in this powershell module +# However when csc compile the inline C#, it will hit process env block size limit since it's not use unicode to encode env +# To solve the env block size problem, we choose to put all inline C# into an assembly VstsTaskSdk.dll, signing it, package with the PS modules. +Write-Verbose "Loading compiled helper $PSScriptRoot\VstsTaskSdk.dll." +Add-Type -LiteralPath $PSScriptRoot\VstsTaskSdk.dll + # Import/export functions. . "$PSScriptRoot\FindFunctions.ps1" . "$PSScriptRoot\InputFunctions.ps1" @@ -31,6 +38,8 @@ Export-ModuleMember -Function @( 'Select-Match' # Input functions. 'Get-Endpoint' + 'Get-SecureFileTicket' + 'Get-SecureFileName' 'Get-Input' 'Get-TaskVariable' 'Get-TaskVariableInfo' @@ -45,6 +54,8 @@ Export-ModuleMember -Function @( 'Write-AddBuildTag' 'Write-AssociateArtifact' 'Write-LogDetail' + 'Write-PrependPath' + 'Write-SetEndpoint' 'Write-SetProgress' 'Write-SetResult' 'Write-SetSecret' @@ -54,8 +65,11 @@ Export-ModuleMember -Function @( 'Write-TaskVerbose' 'Write-TaskWarning' 'Write-UpdateBuildNumber' + 'Write-UpdateReleaseName' 'Write-UploadArtifact' 'Write-UploadBuildLog' + 'Write-UploadFile' + 'Write-UploadSummary' # Out functions. 'Out-Default' # Server OM functions. @@ -65,28 +79,19 @@ Export-ModuleMember -Function @( 'Get-VssCredentials' 'Get-VssHttpClient' # Tool functions. + 'Assert-Agent' 'Assert-Path' 'Invoke-Tool' # Trace functions. 'Trace-EnteringInvocation' 'Trace-LeavingInvocation' 'Trace-Path' + # Proxy functions + 'Get-WebProxy' + # Client cert functions + 'Get-ClientCertificate' ) -# Special internal exception type to control the flow. Not currently intended -# for public usage and subject to change. If the type has already -# been loaded once, then it is not loaded again. -Write-Verbose "Adding exceptions types." -Add-Type -WarningAction SilentlyContinue -Debug:$false -TypeDefinition @' -namespace VstsTaskSdk -{ - public class TerminationException : System.Exception - { - public TerminationException(System.String message) : base(message) { } - } -} -'@ - # Override Out-Default globally. $null = New-Item -Force -Path "function:\global:Out-Default" -Value (Get-Command -CommandType Function -Name Out-Default -ListImported) New-Alias -Name Out-Default -Value "global:Out-Default" -Scope global diff --git a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/lib.json b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/lib.json index b000819..0cde160 100644 --- a/vsts-promotepackage-task/ps_modules/VstsTaskSdk/lib.json +++ b/vsts-promotepackage-task/ps_modules/VstsTaskSdk/lib.json @@ -1,5 +1,6 @@ { "messages": { + "PSLIB_AgentVersion0Required": "Agent version {0} or higher is required.", "PSLIB_ContainerPathNotFound0": "Container path not found: '{0}'", "PSLIB_EndpointAuth0": "'{0}' service endpoint credentials", "PSLIB_EndpointUrl0": "'{0}' service endpoint URL", diff --git a/vsts-promotepackage-task/task.json b/vsts-promotepackage-task/task.json index 6a08163..3e3365a 100644 --- a/vsts-promotepackage-task/task.json +++ b/vsts-promotepackage-task/task.json @@ -16,17 +16,9 @@ "Release" ], "minimumAgentVersion": "1.83.0", - "groups": [ - { - "name": "general", - "displayName": "General Settings", - "isExpanded": true - } - ], "inputs": [ { "name": "feed", - "groupName": "general", "type": "pickList", "label": "Use packages from this VSTS feed", "defaultValue": "", @@ -37,36 +29,71 @@ } }, { - "name": "definition", - "groupName": "general", + "name": "inputType", + "type": "radio", + "label": "Package input type", + "defaultValue": "nameVersion", + "helpMarkDown": "Preferred input type. Either specify the name(s) and version explicitly, or retrieve name(s) and version(s) from metadata of package file(s).", + "required": "true", + "options": { + "nameVersion": "Specify name and version", + "packageFiles": "Get name(s) and version(s) from metadata of package file(s)" + } + }, + { + "name": "packageIds", + "aliases": [ + "definition" + ], "type": "pickList", "label": "Package", "defaultValue": "", - "helpMarkDown": "Select the package that you want to promote", + "helpMarkDown": "Select the package that you want to promote. Multiple packages may be supplied separated with a comma or semicolon. Note you may use either the name of the package or its GUID.", "required": "true", "properties": { - "EditableOptions": "true" - } + "EditableOptions": "true", + "MultiSelectFlatList": "True" + }, + "visibleRule": "inputType = nameVersion" }, { "name": "version", - "groupName": "general", - "type": "pickList", + "type": "string", "label": "Package Version", "defaultValue": "", - "helpMarkDown": "Select the version you want to promote or enter a variable", + "helpMarkDown": "Enter the version you want to promote or a [variable](https://go.microsoft.com/fwlink/?LinkID=550988).", + "required": "true", + "visibleRule": "inputType = nameVersion" + }, + { + "name": "packagesDirectory", + "type": "filePath", + "label": "Package source folder", + "defaultValue": "$(System.DefaultWorkingDirectory)", + "helpMarkDown": "The source folder that the package file pattern(s) will be searched in.", + "required": "true", + "visibleRule": "inputType = packageFiles" + }, + { + "name": "packagesPattern", + "type": "multiLine", + "label": "Package file paths", + "defaultValue": "**/*.nupkg\n!**/*.symbols.nupkg\n**/*.tgz", + "helpMarkDown": "The file paths or glob pattern to match nupkg/tgz files within the package source folder containing the package name and versions (as metadata) that will be promoted. Multiple patterns can be separated by a semicolon or be placed on separate lines.", "required": "true", "properties": { - "EditableOptions": "true" - } + "resizable": "true", + "rows": "10", + "maxLength": "5000" + }, + "visibleRule": "inputType = packageFiles" }, { "name": "releaseView", - "groupName": "general", "type": "pickList", "label": "Release Views", "defaultValue": "", - "helpMarkDown": "Select the view to which to promote the package", + "helpMarkDown": "Select the view to which to promote the package.", "required": "true", "properties": { "EditableOptions": "true" @@ -82,7 +109,7 @@ "resultTemplate": "{ \"Value\" : \"{{{id}}}\", \"DisplayValue\" : \"{{{name}}}\" }" }, { - "target": "definition", + "target": "packageIds", "endpointId": "tfs:feed", "parameters": { "feed": "$(feed)" @@ -91,17 +118,6 @@ "resultSelector": "jsonpath:$.value[*]", "resultTemplate": "{ \"Value\" : \"{{{id}}}\", \"DisplayValue\" : \"{{{name}}}\" }" }, - { - "target": "version", - "endpointId": "tfs:feed", - "parameters": { - "feed": "$(feed)", - "definition": "$(definition)" - }, - "endpointUrl": "{{endpoint.url}}/_apis/Packaging/Feeds/{{{feed}}}/Packages/{{{definition}}}/Versions?includeUrls=false", - "resultSelector": "jsonpath:$.value[*]", - "resultTemplate": "{ \"Value\" : \"{{{version}}}\", \"DisplayValue\" : \"{{{version}}}\" }" - }, { "target": "releaseView", "endpointId": "tfs:feed", diff --git a/vsts-promotepackage-task/vsts-promotepackage-task.ps1 b/vsts-promotepackage-task/vsts-promotepackage-task.ps1 index f6fe04c..acc57c8 100644 --- a/vsts-promotepackage-task/vsts-promotepackage-task.ps1 +++ b/vsts-promotepackage-task/vsts-promotepackage-task.ps1 @@ -4,178 +4,150 @@ param( ) -function InitializeRestHeaders() -{ - $restHeaders = New-Object -TypeName "System.Collections.Generic.Dictionary[[String], [String]]" - if([string]::IsNullOrWhiteSpace($connectedServiceName)) - { - $patToken = GetAccessToken $connectedServiceDetails - ValidatePatToken $patToken - $restHeaders.Add("Authorization", [String]::Concat("Bearer ", $patToken)) - - } - else - { +function InitializeRestHeaders() { + $restHeaders = New-Object -TypeName "System.Collections.Generic.Dictionary[[String], [String]]" + if([string]::IsNullOrWhiteSpace($connectedServiceName)) { + $patToken = GetAccessToken $connectedServiceDetails + ValidatePatToken $patToken + $restHeaders.Add("Authorization", [String]::Concat("Bearer ", $patToken)) + } else { Write-Verbose "Username = $Username" -Verbose - if ($localRun -eq $true) { + if (![string]::IsNullOrWhiteSpace($env:PROMOTEPACKAGE_PAT)) { $Username = "" - $Password = $env:pat - } - else { - $Username = $connectedServiceDetails.Authorization.Parameters.Username + $Password = $env:PROMOTEPACKAGE_PAT + } else { + $Username = $connectedServiceDetails.Authorization.Parameters.Username $Password = $connectedServiceDetails.Authorization.Parameters.Password } - $alternateCreds = [String]::Concat($Username, ":", $Password) - $basicAuth = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($alternateCreds)) - $restHeaders.Add("Authorization", [String]::Concat("Basic ", $basicAuth)) - } - return $restHeaders + $alternateCreds = [String]::Concat($Username, ":", $Password) + $basicAuth = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($alternateCreds)) + $restHeaders.Add("Authorization", [String]::Concat("Basic ", $basicAuth)) + } + return $restHeaders } -function GetAccessToken($vssEndPoint) -{ - $endpoint = (Get-VstsEndpoint -Name SystemVssConnection -Require) - $vssCredential = [string]$endpoint.auth.parameters.AccessToken - return $vssCredential +function GetAccessToken($vssEndPoint) { + $endpoint = (Get-VstsEndpoint -Name SystemVssConnection -Require) + $vssCredential = [string]$endpoint.auth.parameters.AccessToken + return $vssCredential } -function ValidatePatToken($token) -{ - if([string]::IsNullOrWhiteSpace($token)) - { - throw "Unable to generate Personal Access Token for the user. Contact Project Collection Administrator" - } +function ValidatePatToken($token) { + if([string]::IsNullOrWhiteSpace($token)) { + throw "Unable to generate Personal Access Token for the user. Contact Project Collection Administrator" + } } -function Get-FeedId -{ +function Get-FeedId([PSObject]$requestContext, [string]$feedName) { $ret = "" - try - { - $feedUrl = "$basefeedsurl/$feedName/?api-version=5.0-preview.1" + try { + $feedUrl = "$($requestContext.BaseFeedUrl)/$feedName/?api-version=5.0-preview.1" Write-Verbose -Verbose "Trying to retrieve feed information: $feedUrl" - $feedResponse = Invoke-RestMethod -Uri $feedUrl -Headers $headers -ContentType "application/json" -Method Get + $feedResponse = Invoke-RestMethod -Uri $feedUrl -Headers $requestContext.Headers -ContentType "application/json" -Method Get $ret = $feedResponse.id - } - catch - { - throw "Unhandled exception while reading feed $feedName" + } catch { + $ex = [string]::Concat($_.Exception.ToString(), $_.ScriptStackTrace) + throw "Unhandled exception while reading feed $feedName`n$ex" } # If the feed id is empty throw an exception (fallback scenario) - if ([string]::IsNullOrWhiteSpace($ret)) - { + if ([string]::IsNullOrWhiteSpace($ret)) { throw "Feed $feedName could not be found" } return $ret } -function Get-ViewId($FeedId) -{ +function Get-ViewId([PSObject]$requestContext, [string]$FeedId, [string]$releaseView) { $ret = "" - try - { - $viewUrl = "$basefeedsurl/$FeedId/views/$releaseView/?api-version=5.0-preview.1" + try { + $viewUrl = "$($requestContext.BaseFeedUrl)/$FeedId/views/$releaseView/?api-version=5.0-preview.1" Write-Verbose -Verbose "Trying to retrieve view information: $viewUrl" - $viewResponse = Invoke-RestMethod -Uri $viewUrl -Headers $headers -ContentType "application/json" -Method Get + $viewResponse = Invoke-RestMethod -Uri $viewUrl -Headers $requestContext.Headers -ContentType "application/json" -Method Get $ret = $viewResponse.id - } - catch - { - throw "Unhandled exception while reading view $releaseView" + } catch { + $ex = [string]::Concat($_.Exception.ToString(), $_.ScriptStackTrace) + throw "Unhandled exception while reading view $releaseView`n$ex" } # If the view id is empty throw an exception (fallback scenario) - if ([string]::IsNullOrWhiteSpace($ret)) - { + if ([string]::IsNullOrWhiteSpace($ret)) { throw "View $releaseView could not be found" } return $ret } -function Get-PackageInfo($FeedId) -{ +function Get-PackageInfo([PSObject]$requestContext, [string]$FeedId, [string]$packageId) { $name = ""; $protocolType = ""; - #$ret = "" $isId = $true - try - { - $packageUrl = "$basefeedsurl/$FeedId/packages/$packageId/?api-version=5.0-preview.1" + try { + $packageUrl = "$($requestContext.BaseFeedUrl)/$FeedId/packages/$packageId/?api-version=5.0-preview.1" Write-Verbose -Verbose "Trying to retrieve package information: $packageUrl" - $packageResponse = Invoke-RestMethod -Uri $packageUrl -Headers $headers -ContentType "application/json" -Method Get + $packageResponse = Invoke-RestMethod -Uri $packageUrl -Headers $requestContext.Headers -ContentType "application/json" -Method Get $name = $packageResponse.normalizedName $protocolType = $packageResponse.protocolType - #$ret = $packageResponse.id - } - catch [System.Net.WebException] - { + } catch [System.Net.WebException] { $isId = $false - } - catch - { - throw "Unhandled exception while reading package $packageId by id" + } catch { + $ex = [string]::Concat($_.Exception.ToString(), $_.ScriptStackTrace) + throw "Unhandled exception while reading package $packageId by id`n$ex" } # Package not found with id, searching with name - if ($isId -eq $false) - { - try - { + if ($isId -eq $false) { + try { Write-Verbose -Verbose "Package with id $packageId not found, searching with name" - $packagesUrl = "$basefeedsurl/$FeedId/packages?api-version=5.0-preview.1" + $packagesUrl = "$($requestContext.BaseFeedUrl)/$FeedId/packages?api-version=5.0-preview.1" Write-Verbose -Verbose "Retrieving all packages of requested feed: $packagesUrl" - $packagesResponse = Invoke-RestMethod -Uri $packagesUrl -Headers $headers -ContentType "application/json" -Method Get + $packagesResponse = Invoke-RestMethod -Uri $packagesUrl -Headers $requestContext.Headers -ContentType "application/json" -Method Get $packages = $packagesResponse.value - foreach ($package in $packages) - { - if ($package.name -eq $packageId) - { + foreach ($package in $packages) { + if ($package.name -eq $packageId) { $name = $package.normalizedName $protocolType = $package.protocolType + break } } - } - catch - { - Write-Verbose -Verbose "Failed to retrieve package information by name" - throw "Unhandled exception while reading package $packageId by name" + } catch { + $ex = [string]::Concat($_.Exception.ToString(), $_.ScriptStackTrace) + Write-Verbose -Verbose "Failed to retrieve package information by name`n$ex" + throw "Unhandled exception while reading package $packageId by name`n$ex" } } # If the package id is empty throw an exception (fallback scenario) - if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($protocolType)) - { + if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($protocolType)) { throw "Package $packageId could not be found" } - return $name, $protocolType + [PSCustomObject]@{ + Name = $name + FeedType = $protocolType + } } -function Set-PackageQuality -{ +function Set-PackageQuality([PSObject]$requestContext, [string]$feedName, [string]$packageId, [string]$packageVersion, [string]$releaseView) { Write-Host "Promoting version $packageVersion of package $packageId from feed $feedName to view $releaseView" # Get ids for feed, package and view - $feedId = Get-FeedId - $viewId = Get-ViewId -FeedId $feedId - $packageInfo = Get-PackageInfo -FeedId $feedId - $packageName = $packageInfo[0] - $feedType = $packageInfo[1] + $feedId = Get-FeedId -RequestContext $requestContext -FeedName $feedName + $viewId = Get-ViewId -RequestContext $requestContext -FeedId $feedId -ReleaseView $releaseView + $packageInfo = Get-PackageInfo -RequestContext $requestContext -FeedId $feedId -PackageId $packageId + $packageName = $packageInfo.Name + $feedType = $packageInfo.FeedType #API URL is slightly different for npm vs. nuget... - switch($feedType) - { - "npm" { $releaseViewURL = "$basepackageurl/$feedId/$feedType/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } - "nuget" { $releaseViewURL = "$basepackageurl/$feedId/$feedType/packages/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } - "upack" { $releaseViewURL = "$basepackageurl/$feedId/$feedType/packages/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } - "pypi" { $releaseViewURL = "$basepackageurl/$feedId/$feedType/packages/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } - default { $releaseViewURL = "$basepackageurl/$feedId/$feedType/packages/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } + switch($feedType) { + "npm" { $releaseViewURL = "$($requestContext.BasePackageUrl)/$feedId/$feedType/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } + "nuget" { $releaseViewURL = "$($requestContext.BasePackageUrl)/$feedId/$feedType/packages/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } + "upack" { $releaseViewURL = "$($requestContext.BasePackageUrl)/$feedId/$feedType/packages/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } + "pypi" { $releaseViewURL = "$($requestContext.BasePackageUrl)/$feedId/$feedType/packages/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } + default { $releaseViewURL = "$($requestContext.BasePackageUrl)/$feedId/$feedType/packages/$packageName/versions/$($packageVersion)?api-version=5.0-preview.1" } } - $json = @{ + $json = @{ views = @{ op = "add" path = "/views/-" @@ -183,49 +155,133 @@ function Set-PackageQuality } } Write-Host $releaseViewURL - $response = Invoke-RestMethod -Uri $releaseViewURL -Headers $headers -ContentType "application/json" -Method Patch -Body (ConvertTo-Json $json) + $response = Invoke-RestMethod -Uri $releaseViewURL -Headers $requestContext.Headers -ContentType "application/json" -Method Patch -Body (ConvertTo-Json $json) return $response } -function Run() { +function Initalize-RequestContext() { + $basepackageurl = "" + $basefeedsurl = "" + $uri = [uri]$env:SYSTEM_TEAMFOUNDATIONSERVERURI + $hostName = $uri.Host -$url = $env:SYSTEM_TEAMFOUNDATIONSERVERURI -$url = $url.ToLower() + if (($hostName -eq "dev.azure.com") -or ($hostName -eq "vsrm.dev.azure.com")) { + #new style + $account = $uri.Segments[1].TrimEnd('/') # First segment after hostname + $basepackageurl = "https://pkgs.dev.azure.com/$($account)/_apis/packaging/feeds" + $basefeedsurl = "https://feeds.dev.azure.com/$($account)/_apis/packaging/feeds" + } elseif ($hostName.EndsWith("visualstudio.com")) { + #old style + $account = $hostName.Split('.')[0] # First subdomain of hostname + $basepackageurl = "https://$($account).pkgs.visualstudio.com/DefaultCollection/_apis/packaging/feeds" + $basefeedsurl = "https://$($account).feeds.visualstudio.com/DefaultCollection/_apis/packaging/feeds" + } else { + Write-Host "On-Premise TFS / Azure DevOps Server not supported" + } + $headers = InitializeRestHeaders -if ($url -like "https://vsrm.dev.azure.com*") { - #new style - $account = ($env:SYSTEM_TEAMFOUNDATIONSERVERURI -replace "https://vsrm.dev.azure.com/(.*)\/", '$1').split('.')[0] - $basepackageurl = ("https://pkgs.dev.azure.com/{0}/_apis/packaging/feeds" -f $account) - $basefeedsurl = ("https://feeds.dev.azure.com/{0}/_apis/packaging/feeds" -f $account) + [PSCustomObject]@{ + BasePackageUrl = $basepackageurl + BaseFeedUrl = $basefeedsurl + Headers = $headers + } } -elseif ($url -like "https://dev.azure.com*") -{ - #new style - $account = ($env:SYSTEM_TEAMFOUNDATIONSERVERURI -replace "https://dev.azure.com/(.*)\/", '$1').split('.')[0] - $basepackageurl = ("https://pkgs.dev.azure.com/{0}/_apis/packaging/feeds" -f $account) - $basefeedsurl = ("https://feeds.dev.azure.com/{0}/_apis/packaging/feeds" -f $account) + +function New-TemporaryDirectory { + $parent = [System.IO.Path]::GetTempPath() + [string]$name = [System.Guid]::NewGuid() + New-Item -ItemType Directory -Path (Join-Path $parent $name) } -elseif ($url -like "*visualstudio.com*") -{ - #old style - $account = ($env:SYSTEM_TEAMFOUNDATIONSERVERURI -replace "https://(.*)\.visualstudio\.com/", '$1').split('.')[0] - $basepackageurl = ("https://{0}.pkgs.visualstudio.com/DefaultCollection/_apis/packaging/feeds" -f $account) - $basefeedsurl = ("https://{0}.feeds.visualstudio.com/DefaultCollection/_apis/packaging/feeds" -f $account) + +function New-PackageObject { + $package = New-Object -TypeName PSObject + $package | Add-Member -MemberType NoteProperty -Name Name -Value '' + $package | Add-Member -MemberType NoteProperty -Name Version -Value '' + return $package } -else { - Write-Host "On-Premise TFS / Azure DevOps Server not supported" + +Add-Type -AssemblyName System.IO.Compression.FileSystem +function Get-NuGetPackageMetadata([string]$filePath) { + $package = New-PackageObject + $zip = [System.IO.Compression.ZipFile]::OpenRead($filePath) + $tempFilePath = (New-TemporaryFile).FullName + try { + $nuspecEntry = $zip.Entries | Where-Object { $_.FullName -like '*.nuspec' } | Select-Object -First 1 + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($nuspecEntry, $tempFilePath, $true) + $ns = @{ msxml = "http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd" } + $metadata = Select-Xml -Path $tempFilePath -Namespace $ns -XPath "//msxml:metadata" | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty ChildNodes + $package.Name = ($metadata | Where-Object {$_.Name -eq 'id' } | Select-Object -First 1).InnerText + $package.Version = ($metadata | Where-Object {$_.Name -eq 'version' } | Select-Object -First 1).InnerText + } finally { + Remove-Item $tempFilePath -Force -ErrorAction SilentlyContinue + $zip.Dispose() + } + return $package } - $headers=InitializeRestHeaders - Set-PackageQuality +function Get-NpmPackageMetadata([string]$filePath) { + $package = New-PackageObject + $tempDir = New-TemporaryDirectory + try { + if ($IsWindows -eq $null) { + $IsWindows = $Env:OS.StartsWith('Windows') + } + $flags = if ($IsWindows) { '-xvzf' } else { 'xvzf' } + tar $flags `"$filePath`" -C `"$tempDir`" 2> $null + $packageJsonPath = "$tempDir/package/package.json" + $packageJson = Get-Content -Raw -Path $packageJsonPath | ConvertFrom-Json + $package.Name = $packageJson.name + $package.Version = $packageJson.version + } finally { + Remove-Item $tempDir -Force -Recurse -ErrorAction SilentlyContinue + } + return $package } -if ($localRun -eq $false) -{ - $feedName = Get-VstsInput -Name feed - $packageId = Get-VstsInput -Name definition - $packageVersion = Get-VstsInput -Name version - $releaseView =Get-VstsInput -Name releaseView - Run +function Get-PackageMetadata([string]$filePath) { + $extension = [System.IO.Path]::GetExtension($filePath) + if ($extension -eq '.nupkg') { + return Get-NuGetPackageMetadata $filePath + } else { # ($extension -eq '.tgz') + return Get-NpmPackageMetadata $filePath + } } + +function Run() { + $feedName = Get-VstsInput -Name feed -Require + $inputType = Get-VstsInput -Name inputType -Require + $releaseView = Get-VstsInput -Name releaseView -Require + $requestContext = Initalize-RequestContext + + if ($inputType -eq "nameVersion") { + $packageIds = Get-VstsInput -Name packageIds -Require + $packageVersion = Get-VstsInput -Name version -Require + + $ids = $packageIds -Split ',\s*|;\s*' + Write-Host "Promoting $($ids.Length) package(s) named '$packageIds' with version '$packageVersion'" + foreach ($id in $ids) { + Set-PackageQuality -RequestContext $requestContext -FeedName $feedName -PackageId $id -PackageVersion $packageVersion -ReleaseView $releaseView + } + } else { # ($inputType -eq "packageFiles") + $packagesDirectory = Get-VstsInput -Name packagesDirectory -Require + $packagesPattern = Get-VstsInput -Name packagesPattern -Require + + if (!(Test-Path $packagesDirectory)) { + Write-Error "The path '$packagesDirectory' doesn't exist." + } + $patterns = $packagesPattern -Split '\n|;\s*' + Write-Host "Promoting package(s) by reading metadata from package file(s) matching pattern '$patterns' from root directory '$packagesDirectory'" + + $paths = Find-VstsMatch -DefaultRoot $packagesDirectory -Pattern $patterns + Write-Host "Matching paths found:`n$paths" + foreach ($path in $paths) { + $package = Get-PackageMetadata $path + Set-PackageQuality -RequestContext $requestContext -FeedName $feedName -PackageId $package.Name -PackageVersion $package.Version -ReleaseView $releaseView + } + } +} + +if ($localRun -eq $false) { + Run +} \ No newline at end of file