From bc502d23da63bf5ad40dff8d31b431689860c37a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 09:56:42 -0600 Subject: [PATCH 001/130] Platform agnostic candidate; modified compiler get to use versions Refers to #6 --- .../function_Build-ALPackage.ps1 | 24 ++- .../function_Get-BCDependencies.ps1 | 5 + .../function_Expand-Folder.ps1 | 60 ++++++ .../function_Get-VSIXCompiler.ps1 | 190 ++++++++++-------- .../wrapper_Get-VSIXCompiler.ps1 | 5 +- 5 files changed, 197 insertions(+), 87 deletions(-) create mode 100644 bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index 5c104dd..0611718 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -47,17 +47,31 @@ function Build-ALPackage { ) try { - $alcPath = Join-Path -Path $ALEXEPath -ChildPath "win32" - $alcReference = Join-Path -Path $alcPath -ChildPath "ALC.EXE" + if ($IsWindows) { + $alcPath = Join-Path -Path $ALEXEPath -ChildPath "win32" + $alcReference = Join-Path -Path $alcPath -ChildPath "ALC.EXE" + } elseif ($IsLinux) { + $alcPath = Join-Path -Path $ALEXEPath -ChildPath "linux" + $alcReference = Join-Path -Path $alcPath -ChildPath "ALC" + } else { + Write-Error "This is being run on an unsupported agent; exiting" + exit 1 + } } catch { - throw "An error occurred; it doesn't seem that $alcPath contains a folder called 'win32', or the folder $alcPath\win32 doesn't contain ALC.EXE" + if ($IsWindows) { + throw "An error occurred; it doesn't seem that $alcPath contains a folder called 'win32', or the folder $alcPath\win32 doesn't contain ALC.EXE" + } elseif ($IsLinux) { + throw "An error occurred; it doesn't seem that $alcPath contains a folder called 'linux', or the folder $alcPath\linux doesn't contain ALC" + } else { + throw "A different error has occurred: $($_.Exception.Message)" + } } if (-not (Test-Path -Path $alcReference)){ - throw "ALC.EXE not found in $alcPath" + throw "ALC[.EXE] not found in $alcPath" } else { - Write-Host "Found ALC.EXE at $alcPath" + Write-Host "Found ALC[.EXE] at $alcPath" } if (-not (Test-Path -Path $PackagesDirectory)) { diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 index 7a99846..0be3e5e 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 @@ -206,6 +206,11 @@ function Get-BCDependencies { Write-Host "Dependency $appName ($($dependency.id)) downloaded" } + if ($IsLinux) { + Write-Host "Executing chmod to allow access for all of the extracted files" + chmod -R 644 $$TopExtractedFolder + } + ######################################################################################################################################## # Internal function here ######################################################################################################################################## diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 new file mode 100644 index 0000000..26e5d60 --- /dev/null +++ b/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 @@ -0,0 +1,60 @@ +function Expand-Folder { + param( + [Parameter(Mandatory)] + [String]$FileName, + [Parameter(Mandatory)] + [String]$ExtractFolder, + [Parameter()] + [String]$TopExtractedFolder = "expanded" + ) + + Add-Type -AssemblyName System.IO.Compression.FileSystem + + $extractionPath = Join-Path -Path (Split-Path $FileName) -ChildPath $TopExtractedFolder + $targetPrefix = if ($ExtractFolder.EndsWith("/")) { + $ExtractFolder + } else { + "$ExtractFolder/" + } + + Write-Host "Expanding '$FileName' folder '$targetPrefix' to '$extractionPath'" + + $fs = [System.IO.File]::OpenRead($FileName) + $zip = [System.IO.Compression.ZipArchive]::new($fs, [System.IO.Compression.ZipArchiveMode]::Read) + + try { + $subfolder = Split-Path -Path $targetPrefix -Leaf + + foreach ($entry in $zip.Entries) { + if ($entry.FullName.StartsWith($targetPrefix)) { + + $relativePath = $entry.FullName.Substring($targetPrefix.Length) + + if (-not [string]::IsNullOrEmpty($relativePath)) { + $destpath = Join-Path -Path (Join-Path -Path $extractionPath -ChildPath $subfolder) -ChildPath $relativePath + + $destDir = Split-Path -Path $destPath -Parent + if (-not (Test-Path $destDir)) { + New-Item -ItemType Directory -Path $destDir -Force | Out-Null + } + Write-Host "Extracting file: $entry" + $outStream = [System.IO.File]::Create($destPath) + $entry.Open().CopyTo($outStream) + $outStream.Close() + } + } + } + } + catch { + Write-Error "Error: $($_.Exception.Message)" + exit 1 + } + finally { + $fs.Close() + } + + if ($IsLinux) { + Write-Host "Executing chmod to allow access for all of the extracted files" + chmod -R 755 $$TopExtractedFolder + } +} diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 438dec7..b54dd85 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -1,110 +1,138 @@ -<# -.SYNOPSIS - [WINDOWS ONLY] This function is used to collect the latest version of the ms-dynamics-smb.al package from the Visual Studio Marketplace, decompress it, - and return the directory reference -.DESCRIPTION - [WINDOWS ONLY] - The function goes out to the web and collects the version information from the web, then subsequently uses that information to download the actual copy of - the .VSIX file from the marketplace API. It then renames the file as a .zip file, and extracts it to a directory, then provides the directory and version - as a response. This is a Windows only routine, as it relies upon DOM to parse the response on the first query (as though it were a user.) This is - required because the actual website responds with a series of JS and other reactive components, and ms-dynamics-smb.al is not listed in the marketplace - query API. -.PARAMETER DownloadDirectory - The directory in which to stage the artifacts that are being downloaded; the routine will automatically place the decompiled result in this directory with - a subdirectory of "/expanded" -.OUTPUTS - PSCustomObject containing ALEXEPath (the path to the decompiled file) and Version (the version of the decompiled file) -.NOTES - SEE DESCRIPTION FOR WINDOWS ONLY REQUIREMENT - Author : James McCullough - Company : Evergrowth Consulting - Version : 1.0.0 - Created : 2025-05-21 - Purpose : DevOps-compatible AL extension download and decompression -#> -function Get-VSIXCompiler { +function Get-VSIXCompilerVersion { param( - [Parameter(Mandatory)] - [String]$DownloadDirectory + [Parameter()] + [String]$Version = 'latest', + [Parameter()] + [String]$DownloadDirectory = ".", + [Parameter()] + [Switch]$DebugMode ) - if (-not (Test-Path -Path $downloadDirectory)) { - New-Item -ItemType Directory -Path $DownloadDirectory - } - - # Initialize variables - $coreUrl = "https://marketplace.visualstudio.com/items?itemName=ms-dynamics-smb.al" - $downloadUrlPrototype = "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/vsextensions/al/%VERSION%/vspackage" - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - - $targetPackageDirectory = $downloadDirectory - $targetExpansionSubdirectory = Join-Path -Path $targetPackageDirectory -ChildPath "expanded" - $ProgressPreference = 'SilentlyContinue' - # Step 1: Get version information and session cookie - try { - $webrequest = Invoke-WebRequest -Uri $coreUrl -SessionVariable websession - - $HTML = New-Object -Com "HTMLFile" - [string]$htmlBody = $webrequest.Content - $HTML.Write([ref]$htmlBody) + Write-Host "Determining platform" + $platform = if ($IsWindows) { + "win32" + } + elseif ($IsLinux) { + "linux" + } + else { + Write-Error "Unsupported platform; this routine only supports Linux or Windows" + exit 1 + } + Write-Host "Detected platform: $platform" + + $apiUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" + + Write-Host "Contacting '$apiUrl'" + + $jsonRawPrototype = [ordered]@{ + filters = @( + @{ + criteria = @( + @{ + filterType = 7 + value = 'ms-dynamics-smb.al' + } + ) + pageNumber = 1 + pageSize = 100 + sortBy = 0 + sortOrder = 0 + } + ) + assetTypes = @() + flags = 129 + } | ConvertTo-Json -Depth 10 + try { + $headers = @{"Accept" = "application/json; charset=utf-8;api-version=7.2-preview.1" } + $restResult = Invoke-RestMethod -Uri $apiUrl -Method Post -Body $jsonRawPrototype -ContentType "application/json" -Headers $headers } catch { - Write-Host "An error occurred during the attempt to resolve the latest version: $($_.Exception.Message)" - throw + Write-Error "An error occurred: $($_.Exception.Message)" + exit 1 } - $VSIXRefs = $HTML.scripts | Where-Object className -eq "jiContent" | Select-Object -ExpandProperty innerHTML | ConvertFrom-Json - - if (-not $VSIXRefs -or -not $VSIXRefs.Resources) { - Write-Host "Was unable to resolve the VSIX references required; exiting with error" - throw "Unable to resolve VSIX references" + if (-not $restResult) { + Write-Error "Something went wrong, didn't get a proper response from the API" + exit 1 } - Write-Host "Found VSIX reference for $($VSIXRefs.Resources.ExtensionName) with a version number $($VSIXRefs.Resources.Version)" - - $downloadUrl = $downloadUrlPrototype.Replace("%VERSION%", $VSIXRefs.Resources.Version) - Write-Host "Downloading from $downloadUrl" + Write-Host "Received response from the API with $($restResult.results[0].extensions[0].versions.Count) versions coming back" - # Step 2: Attempt to download the target file - try { - $response = Invoke-WebRequest -Uri $downloadurl -WebSession $websession + $publisher = $restResult.results[0].extensions[0].publisher.publisherName + $extension = $restResult.results[0].extensions[0].ExtensionName + + if ($Version -eq 'latest') { + $getVersion = $restResult.results[0].extensions[0].versions[0].version } - catch { - Write-Host "An error occurred trying to download the actual package from $downloadUrl" - Write-Host "The error: $($_.Exception.Message)" - throw + else { + $versions = $restResult.results[0].extensions[0].versions + $versionExists = $versions.version -contains $Version + if ($versionExists) { + $getVersion = $Version + } + else { + Write-Error "Version $Version was not found in the list of versions; please check your version number or try 'latest'" + exit 1 + } } - $responseHeaderCD = $response.Headers['Content-Disposition'] - $disposition = [System.Net.Mime.ContentDisposition]::new($responseHeaderCD) - $dispositionFilePath = Join-Path -Path $targetPackageDirectory -ChildPath $disposition.FileName - [IO.File]::WriteAllBytes($dispositionFilePath, $response.Content) + Write-Host "Acquiring compiler with the following metadata:" + Write-Host (" {0,-20} = {1}" -f "publisher", $publisher) + Write-Host (" {0,-20} = {1}" -f "extension", $extension) + Write-Host (" {0,-20} = {1}" -f "version", $version) + Write-Host "" + + $downloadUrl = "https://$($publisher).gallery.vsassets.io/_apis/public/gallery/publisher/$($publisher)/extension/$($extension)/$($getVersion)/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage" + Write-Host "Acquisition: $downloadUrl" - Write-Host "Downloaded $($disposition.FileName): $($response.RawContentLength) bytes" + $target = Join-Path -Path $DownloadDirectory -ChildPath "compiler.vsix" + Write-Host "Download target: $target" + + if (-not $DebugMode) { + Invoke-RestMethod -Uri $downloadUrl -Method Get -ContentType "application/json" -OutFile $target + Write-Host "Downloaded file: $target" + } # Step 3: Rename file because Azure Pipelines' version of Expand-Archive is a little b**** - $newFileName = "$($disposition.FileName).zip" - Rename-Item -Path $dispositionFilePath -NewName $newFileName -Force - $dispositionFilePath = $dispositionFilePath + ".zip" + $newFileName = "compiler.zip" + $newPath = Join-Path -Path (Split-Path $target) -ChildPath $newFileName + Rename-Item -Path $target -NewName $newPath -Force + + Write-Host "Renamed '$target' to '$newFileName' for unzipping" - # Step 4: Expand .vsix file to file system to extract ALC.EXE - $downloadedFile = $dispositionFilePath - $expansionPath = $targetExpansionSubdirectory - New-Item -ItemType Directory -Path $expansionPath -Force | Out-Null + Write-Host "Extracting folder for '$platform' environment from VSIX" - Expand-Archive -Path $downloadedFile -DestinationPath $expansionPath -Force + $expandFolder = "expanded" + + Expand-Folder -FileName $newPath -ExtractFolder "extension/bin/$platform" -TopExtractedFolder $expandFolder + + Expand-Folder -FileName $newPath -ExtractFolder "extension/bin/Analyzers" -TopExtractedFolder $expandFolder # Step 5: Finish - $ALEXEPath = Join-Path -Path $expansionPath -ChildPath (Join-Path -Path "extension" -ChildPath "bin") - Write-Host "Routine complete; ALC.EXE should be located at $ALEXEPath" + $expectedEnvPath = if ($IsWindows) { + "win32" + } else { + "linux" + } + + $expectedCompilerName = if ($IsWindows) { + "alc.exe" + } else { + "alc" + } + + $ALEXEPath = Join-Path -Path $expandFolder -ChildPath (Join-Path -Path $expectedEnvPath -ChildPath $expectedCompilerName) + + Write-Host "Routine complete; ALC[.EXE] should be located at $ALEXEPath" Write-Host "Returning ALEXEPath from to function call" return [PSCustomObject]@{ ALEXEPath = $ALEXEPath - Version = $VSIXRefs.Resources.Version + Version = $getVersion } -} +} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index 507df47..1541346 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -1,11 +1,14 @@ . "./function_Get-VSIXCompiler.ps1" +. "./function_Expand_Folder.ps1" $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require +$localCompilerVersion = Get-VstsInput -Name 'Version' - Require Write-Host "Getting AL Compiler:" Write-Host (" {0,-20} = {1}" -f "DownloadDirectory", $localDownloadDirectory) +Write-Host (" {0,-20} = {1}" -f "Version", $localCompilerVersion) -$vsixResult = Get-VSIXCompiler -DownloadDirectory $localDownloadDirectory +$vsixResult = Get-VSIXCompilerVersion -DownloadDirectory $localDownloadDirectory -Version $localCompilerVersion if (-not $vsixResult -or ` [string]::IsNullOrWhiteSpace($vsixResult.Version) -or ` From b00bc751828254bb33b34d404a395003514e421c Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 10:26:29 -0600 Subject: [PATCH 002/130] Missed updating task.json for the compiler. Refers to #6 --- bc-tools-extension/Get-VSIXCompiler/task.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bc-tools-extension/Get-VSIXCompiler/task.json b/bc-tools-extension/Get-VSIXCompiler/task.json index cca19e5..46bccf4 100644 --- a/bc-tools-extension/Get-VSIXCompiler/task.json +++ b/bc-tools-extension/Get-VSIXCompiler/task.json @@ -20,6 +20,14 @@ "defaultValue": "$(Build.ArtifactStagingDirectory)", "required": true, "helpMarkDown": "The download folder of the compiler. Default: $(Build.ArtifactStagingDirectory)" + }, + { + "name": "Version", + "type": "string", + "label": "Target Version", + "defaultValue": "($Build.ArtifactStagingDirectory)/expanded", + "required": true, + "helpMarkDown": "This is the top level folder name for the expanded compiler. Default: $(Build.ArtifactStagingDirectory)/expanded" } ], "execution": { From ba74b3e76cf0613bee1504cfd3f33004a128952c Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 10:31:26 -0600 Subject: [PATCH 003/130] Missed two more references in the task.json for the expanded functionality --- bc-tools-extension/Get-VSIXCompiler/task.json | 12 ++++++++++-- .../Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/task.json b/bc-tools-extension/Get-VSIXCompiler/task.json index 46bccf4..e61a659 100644 --- a/bc-tools-extension/Get-VSIXCompiler/task.json +++ b/bc-tools-extension/Get-VSIXCompiler/task.json @@ -25,9 +25,17 @@ "name": "Version", "type": "string", "label": "Target Version", - "defaultValue": "($Build.ArtifactStagingDirectory)/expanded", + "defaultValue": "latest", "required": true, - "helpMarkDown": "This is the top level folder name for the expanded compiler. Default: $(Build.ArtifactStagingDirectory)/expanded" + "helpMarkDown": "The exact version to download, or 'latest' to get the latest. Default: 'latest'" + }, + { + "name": "ExpansionDirectory", + "type": "string", + "label": "Expansion Directory", + "defaultValue": "expanded", + "required": false, + "helpMarkDown": "The top level directory name for the expanded files. Default: 'expanded'" } ], "execution": { diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index 1541346..765b7d1 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -3,12 +3,14 @@ $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require $localCompilerVersion = Get-VstsInput -Name 'Version' - Require +$localTopLevelDirectory = Get-VstsInput -Name 'ExpansionDirectory' Write-Host "Getting AL Compiler:" Write-Host (" {0,-20} = {1}" -f "DownloadDirectory", $localDownloadDirectory) Write-Host (" {0,-20} = {1}" -f "Version", $localCompilerVersion) +Write-Host (" {0,-20} = {1}" -f "ExpansionDirectory", $localTopLevelDirectory) -$vsixResult = Get-VSIXCompilerVersion -DownloadDirectory $localDownloadDirectory -Version $localCompilerVersion +$vsixResult = Get-VSIXCompilerVersion -DownloadDirectory $localDownloadDirectory -Version $localCompilerVersion -TopExtractedFolder $localTopLevelDirectory if (-not $vsixResult -or ` [string]::IsNullOrWhiteSpace($vsixResult.Version) -or ` From 9d21846368b7e4e8ee9b4a349fcfae8846e9799a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 10:39:51 -0600 Subject: [PATCH 004/130] Fix catastrophic spelling error Refers to #6 --- .../Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index 765b7d1..9abd155 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -1,5 +1,5 @@ . "./function_Get-VSIXCompiler.ps1" -. "./function_Expand_Folder.ps1" +. "./function_Expand-Folder.ps1" $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require $localCompilerVersion = Get-VstsInput -Name 'Version' - Require From f93f599d3ca3c703ab5d2e7bd07310852aa1ef66 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 11:03:21 -0600 Subject: [PATCH 005/130] So many typos Refers to #6 --- .../Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index 9abd155..b478908 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -2,7 +2,7 @@ . "./function_Expand-Folder.ps1" $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require -$localCompilerVersion = Get-VstsInput -Name 'Version' - Require +$localCompilerVersion = Get-VstsInput -Name 'Version' -Require $localTopLevelDirectory = Get-VstsInput -Name 'ExpansionDirectory' Write-Host "Getting AL Compiler:" From 49c3cfdc547aadfbe7c5315323ff716c7d94d98d Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 11:16:17 -0600 Subject: [PATCH 006/130] Remove mistaken parameter from task.json Refers to #6 --- bc-tools-extension/Get-VSIXCompiler/task.json | 10 +--------- .../Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 | 4 +--- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/task.json b/bc-tools-extension/Get-VSIXCompiler/task.json index e61a659..9d136eb 100644 --- a/bc-tools-extension/Get-VSIXCompiler/task.json +++ b/bc-tools-extension/Get-VSIXCompiler/task.json @@ -28,15 +28,7 @@ "defaultValue": "latest", "required": true, "helpMarkDown": "The exact version to download, or 'latest' to get the latest. Default: 'latest'" - }, - { - "name": "ExpansionDirectory", - "type": "string", - "label": "Expansion Directory", - "defaultValue": "expanded", - "required": false, - "helpMarkDown": "The top level directory name for the expanded files. Default: 'expanded'" - } + } ], "execution": { "PowerShell3": { diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index b478908..f1bc369 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -3,14 +3,12 @@ $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require $localCompilerVersion = Get-VstsInput -Name 'Version' -Require -$localTopLevelDirectory = Get-VstsInput -Name 'ExpansionDirectory' Write-Host "Getting AL Compiler:" Write-Host (" {0,-20} = {1}" -f "DownloadDirectory", $localDownloadDirectory) Write-Host (" {0,-20} = {1}" -f "Version", $localCompilerVersion) -Write-Host (" {0,-20} = {1}" -f "ExpansionDirectory", $localTopLevelDirectory) -$vsixResult = Get-VSIXCompilerVersion -DownloadDirectory $localDownloadDirectory -Version $localCompilerVersion -TopExtractedFolder $localTopLevelDirectory +$vsixResult = Get-VSIXCompilerVersion -DownloadDirectory $localDownloadDirectory -Version $localCompilerVersion if (-not $vsixResult -or ` [string]::IsNullOrWhiteSpace($vsixResult.Version) -or ` From 4fa8c0b7ec6e79c7c6e523c7d501d5d5dcbf1c69 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 12:46:30 -0600 Subject: [PATCH 007/130] Change windows detection because Agents don't use normal Windows or something? --- .../function_Build-ALPackage.ps1 | 13 +++++--- .../function_Get-VSIXCompiler.ps1 | 31 +++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index 0611718..fea80ed 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -47,10 +47,13 @@ function Build-ALPackage { ) try { - if ($IsWindows) { + if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { $alcPath = Join-Path -Path $ALEXEPath -ChildPath "win32" $alcReference = Join-Path -Path $alcPath -ChildPath "ALC.EXE" - } elseif ($IsLinux) { + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + $alcPath = Join-Path -Path $ALEXEPath -ChildPath "win32" + $alcReference = Join-Path -Path $alcPath -ChildPath "ALC.EXE" + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { $alcPath = Join-Path -Path $ALEXEPath -ChildPath "linux" $alcReference = Join-Path -Path $alcPath -ChildPath "ALC" } else { @@ -59,9 +62,11 @@ function Build-ALPackage { } } catch { - if ($IsWindows) { + if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + throw "An error occurred; it doesn't seem that $alcPath contains a folder called 'win32', or the folder $alcPath\win32 doesn't contain ALC.EXE" + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { throw "An error occurred; it doesn't seem that $alcPath contains a folder called 'win32', or the folder $alcPath\win32 doesn't contain ALC.EXE" - } elseif ($IsLinux) { + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { throw "An error occurred; it doesn't seem that $alcPath contains a folder called 'linux', or the folder $alcPath\linux doesn't contain ALC" } else { throw "A different error has occurred: $($_.Exception.Message)" diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index b54dd85..4224cbf 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -12,16 +12,20 @@ function Get-VSIXCompilerVersion { $ProgressPreference = 'SilentlyContinue' Write-Host "Determining platform" - $platform = if ($IsWindows) { - "win32" + if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + $platform = "win32" } - elseif ($IsLinux) { - "linux" + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + $platform = "win32" + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { + $platform = "linux" } else { - Write-Error "Unsupported platform; this routine only supports Linux or Windows" + Write-Error "Unsupported platform: $([System.Runtime.InteropServices.RuntimeInformation]::OSDescription)" exit 1 } + Write-Host "Detected platform: $platform" $apiUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" @@ -114,18 +118,27 @@ function Get-VSIXCompilerVersion { Expand-Folder -FileName $newPath -ExtractFolder "extension/bin/Analyzers" -TopExtractedFolder $expandFolder # Step 5: Finish - $expectedEnvPath = if ($IsWindows) { + $expectedEnvPath = if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + "win32" + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { "win32" - } else { + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { "linux" } - $expectedCompilerName = if ($IsWindows) { + $expectedCompilerName = if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + "alc.exe" + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { "alc.exe" - } else { + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { "alc" } + $ALEXEPath = Join-Path -Path $expandFolder -ChildPath (Join-Path -Path $expectedEnvPath -ChildPath $expectedCompilerName) Write-Host "Routine complete; ALC[.EXE] should be located at $ALEXEPath" From 967a64b755b4c5528f7d5900ac8ebba219599575 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 12:56:33 -0600 Subject: [PATCH 008/130] Make directory-safe entry --- .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 4224cbf..0cea549 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -94,6 +94,11 @@ function Get-VSIXCompilerVersion { $downloadUrl = "https://$($publisher).gallery.vsassets.io/_apis/public/gallery/publisher/$($publisher)/extension/$($extension)/$($getVersion)/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage" Write-Host "Acquisition: $downloadUrl" + if (-not (Test-Path -Path $DownloadDirectory)) { + New-Item -ItemType Directory -Name $DownloadDirectory + Write-Host "Creating directory: $DownloadDirectory" + } + $target = Join-Path -Path $DownloadDirectory -ChildPath "compiler.vsix" Write-Host "Download target: $target" From beb90309ef89444024cee36278bb966845480270 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 15:09:14 -0600 Subject: [PATCH 009/130] Add common tools function but don't fully implement yet Refers to #6 --- .../wrapper_Get-VSIXCompiler.ps1 | 6 ++++ bc-tools-extension/_common/CommonTools.ps1 | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 bc-tools-extension/_common/CommonTools.ps1 diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index f1bc369..6f6de60 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -1,5 +1,7 @@ . "./function_Get-VSIXCompiler.ps1" . "./function_Expand-Folder.ps1" +. (Join-Path -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -ChildPath "../_common/CommonTools.ps1") + $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require $localCompilerVersion = Get-VstsInput -Name 'Version' -Require @@ -8,6 +10,10 @@ Write-Host "Getting AL Compiler:" Write-Host (" {0,-20} = {1}" -f "DownloadDirectory", $localDownloadDirectory) Write-Host (" {0,-20} = {1}" -f "Version", $localCompilerVersion) +Write-Host "Normalizing directory reference: $localDownloadDirectory" +$localDownloadDirectory = ConvertFrom-DevopsPath $localDownloadDirectory +Write-Host "Normalized directory reference: $localDownloadDirectory" + $vsixResult = Get-VSIXCompilerVersion -DownloadDirectory $localDownloadDirectory -Version $localCompilerVersion if (-not $vsixResult -or ` diff --git a/bc-tools-extension/_common/CommonTools.ps1 b/bc-tools-extension/_common/CommonTools.ps1 new file mode 100644 index 0000000..b6aacad --- /dev/null +++ b/bc-tools-extension/_common/CommonTools.ps1 @@ -0,0 +1,29 @@ + +function ConvertFrom-DevopsPath { + param([Parameter(Mandatory)][string]$Path) + if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { + return [System.IO.Path]::GetFullPath($Path) + } else { + return $null + } +} + +function Get-OSEnvironment { + if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + return "win32" + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + return "win" + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { + return "linux" + } + else { + return "unknown" + } +} + From 0433f19cec79acfa17c868ee2dc4564662597caf Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 15:15:08 -0600 Subject: [PATCH 010/130] Okay, apparently we can't have nice things because Microsoft --- .../Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index 6f6de60..09bc615 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -1,7 +1,18 @@ +function ConvertFrom-DevopsPath { + param([Parameter(Mandatory)][string]$Path) + if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { + return [System.IO.Path]::GetFullPath($Path) + } else { + return $null + } +} + . "./function_Get-VSIXCompiler.ps1" . "./function_Expand-Folder.ps1" -. (Join-Path -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -ChildPath "../_common/CommonTools.ps1") - $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require $localCompilerVersion = Get-VstsInput -Name 'Version' -Require From 7b228e09a70ab840668569b1ab089a0201a31623 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 15:22:26 -0600 Subject: [PATCH 011/130] ***** wept; there's a difference between -Name and -Path --- .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 0cea549..af1492a 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -95,7 +95,7 @@ function Get-VSIXCompilerVersion { Write-Host "Acquisition: $downloadUrl" if (-not (Test-Path -Path $DownloadDirectory)) { - New-Item -ItemType Directory -Name $DownloadDirectory + New-Item -ItemType Directory -Path $DownloadDirectory Write-Host "Creating directory: $DownloadDirectory" } From c465cfe10cc3f2914d0c6adc93d70aac1bc47674 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 15:39:47 -0600 Subject: [PATCH 012/130] I love platform agnositicism --- .../Get-VSIXCompiler/function_Expand-Folder.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 index 26e5d60..55a94dc 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 @@ -20,7 +20,10 @@ function Expand-Folder { Write-Host "Expanding '$FileName' folder '$targetPrefix' to '$extractionPath'" $fs = [System.IO.File]::OpenRead($FileName) - $zip = [System.IO.Compression.ZipArchive]::new($fs, [System.IO.Compression.ZipArchiveMode]::Read) + $zipCtor = [System.IO.Compression.ZipArchive].GetConstructor( + @([System.IO.Stream], [System.IO.Compression.ZipArchiveMode]) + ) + $zip = $zipCtor.Invoke(@($fs, [System.IO.Compression.ZipArchiveMode]::Read)) try { $subfolder = Split-Path -Path $targetPrefix -Leaf From 46e685f7946889a79b064ff62dc8ca339ccbe5ed Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 16:00:02 -0600 Subject: [PATCH 013/130] Deactivate targeted folder expansion; it was causing problems --- .../function_Get-VSIXCompiler.ps1 | 31 ++++++++++++------- .../wrapper_Get-VSIXCompiler.ps1 | 2 +- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index af1492a..e66a47b 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -118,9 +118,12 @@ function Get-VSIXCompilerVersion { $expandFolder = "expanded" - Expand-Folder -FileName $newPath -ExtractFolder "extension/bin/$platform" -TopExtractedFolder $expandFolder - - Expand-Folder -FileName $newPath -ExtractFolder "extension/bin/Analyzers" -TopExtractedFolder $expandFolder + try { + Expand-Archive -Path $FileName -DestinationPath $extractionPath -Force + } catch { + Write-Error "Expand-Archive failed: $($_.Exception.Message)" + exit 1 + } # Step 5: Finish $expectedEnvPath = if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { @@ -143,14 +146,20 @@ function Get-VSIXCompilerVersion { "alc" } + $ALEXEPath = Join-Path -Path $expandFolder -ChildPath (Join-Path -Path "extension" -ChildPath (Join-Path -Path "bin" -ChildPath (Join-Path -Path $expectedEnvPath -ChildPath $expectedCompilerName))) - $ALEXEPath = Join-Path -Path $expandFolder -ChildPath (Join-Path -Path $expectedEnvPath -ChildPath $expectedCompilerName) - - Write-Host "Routine complete; ALC[.EXE] should be located at $ALEXEPath" - Write-Host "Returning ALEXEPath from to function call" - - return [PSCustomObject]@{ - ALEXEPath = $ALEXEPath - Version = $getVersion + Write-Host "Testing destination: $ALEXEPath" + if (Test-Path -Path $ALEXEPath) { + Write-Host "Routine complete; ALC[.EXE] should be located at $ALEXEPath" + Write-Host "Returning ALEXEPath from to function call" + + return [PSCustomObject]@{ + ALEXEPath = $ALEXEPath + Version = $getVersion + } + } else { + Write-Error "'$ALEXEPath' did not resolve to a correct location. Enumerating file system for reference:" + Write-Host "" + Get-ChildItem -Path $(Build.SourcesDirectory)\*.* -Force -Recurse | %{$_.FullName} } } \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index 09bc615..943f7b3 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -12,7 +12,7 @@ function ConvertFrom-DevopsPath { } . "./function_Get-VSIXCompiler.ps1" -. "./function_Expand-Folder.ps1" +#. "./function_Expand-Folder.ps1" $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require $localCompilerVersion = Get-VstsInput -Name 'Version' -Require From 89fe707679067a882779d2f19c06ee5d107a0cb2 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 16:06:58 -0600 Subject: [PATCH 014/130] Misnamed variable; what next? --- .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index e66a47b..3049c96 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -119,7 +119,7 @@ function Get-VSIXCompilerVersion { $expandFolder = "expanded" try { - Expand-Archive -Path $FileName -DestinationPath $extractionPath -Force + Expand-Archive -Path $FileName -DestinationPath $expandFolder -Force } catch { Write-Error "Expand-Archive failed: $($_.Exception.Message)" exit 1 From eef0ce001727f6a9eecb64dad8040b6ec72d4248 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 16:16:30 -0600 Subject: [PATCH 015/130] I must be getting tired --- .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 3049c96..561a150 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -119,7 +119,7 @@ function Get-VSIXCompilerVersion { $expandFolder = "expanded" try { - Expand-Archive -Path $FileName -DestinationPath $expandFolder -Force + Expand-Archive -Path $newPath -DestinationPath $expandFolder -Force } catch { Write-Error "Expand-Archive failed: $($_.Exception.Message)" exit 1 From d240f1db0c04fcb63caad688ddc01e29e9b41305 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 16:36:53 -0600 Subject: [PATCH 016/130] Spacing on log outputs; add app.json validation exposure --- .../Get-BCDependencies/function_Get-BCDependencies.ps1 | 7 +++++++ .../Get-BCDependencies/wrapper_Get-BCDependencies.ps1 | 4 ++-- .../Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 | 8 ++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 index 0be3e5e..73f9faf 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 @@ -101,6 +101,13 @@ function Get-BCDependencies { Write-Verbose "converted app.json; confirming now" + Write-Host "Found the following before validation routine:" + $appJsonExists = if ($appJSON) {"true"} else {"false"} + Write-Host (" {0,-30} = {1}" -f "app.json exists", $appJsonExists) + Write-Host (" {0,-30} = {1}" -f "app.id", $appJSON.id) + Write-Host (" {0,-30} = {1}" -f "app.name", $appJSON.name) + Write-Host (" {0,-30} = {1}" -f "app.publisher", $appJSON.publisher) + if (-not $appJSON -or -not $appJSON.id -or -not $appJSON.name -or -not $appJSON.publisher) { throw "app.json found at '$appJSONFile' does not appear to be valid; exiting..." } diff --git a/bc-tools-extension/Get-BCDependencies/wrapper_Get-BCDependencies.ps1 b/bc-tools-extension/Get-BCDependencies/wrapper_Get-BCDependencies.ps1 index 15ce925..3f066d5 100644 --- a/bc-tools-extension/Get-BCDependencies/wrapper_Get-BCDependencies.ps1 +++ b/bc-tools-extension/Get-BCDependencies/wrapper_Get-BCDependencies.ps1 @@ -30,10 +30,10 @@ $switchParams["PathToPackagesDirectory"] = $local_PathToPackagesDirectory Write-Host "Getting AL Dependencies:" $switchParams.GetEnumerator() | ForEach-Object { if ($_.Key -eq "ClientSecret") { - Write-Host (" {0,-20} = {1}" -f $_.Key, "Are you nuts?") + Write-Host (" {0,-30} = {1}" -f $_.Key, "Are you nuts?") } else { - Write-Host (" {0,-20} = {1}" -f $_.Key, $_.Value) + Write-Host (" {0,-30} = {1}" -f $_.Key, $_.Value) } } diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index 943f7b3..9218ba3 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -18,8 +18,8 @@ $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require $localCompilerVersion = Get-VstsInput -Name 'Version' -Require Write-Host "Getting AL Compiler:" -Write-Host (" {0,-20} = {1}" -f "DownloadDirectory", $localDownloadDirectory) -Write-Host (" {0,-20} = {1}" -f "Version", $localCompilerVersion) +Write-Host (" {0,-30} = {1}" -f "DownloadDirectory", $localDownloadDirectory) +Write-Host (" {0,-30} = {1}" -f "Version", $localCompilerVersion) Write-Host "Normalizing directory reference: $localDownloadDirectory" $localDownloadDirectory = ConvertFrom-DevopsPath $localDownloadDirectory @@ -36,7 +36,7 @@ if (-not $vsixResult -or ` } Write-Host "Variable assignments being set:" -Write-Host (" {0,-20} = {1}" -f "alVersion", $vsixResult.Version) +Write-Host (" {0,-30} = {1}" -f "alVersion", $vsixResult.Version) Write-Host "##vso[task.setvariable variable=alVersion;isOutput=true]$vsixResult.Version" -Write-Host (" {0,-20} = {1}" -f "alPath", $vsixResult.ALEXEPath) +Write-Host (" {0,-30} = {1}" -f "alPath", $vsixResult.ALEXEPath) Write-Host "##vso[task.setvariable variable=alPath;isOutput=true]$vsixResult.ALEXEPath" From 61760f3f1e710fc34b4378a0fff1c7acace82220 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 16:51:50 -0600 Subject: [PATCH 017/130] Remove dead platform handling attempts from selective extraction piece --- .../function_Build-ALPackage.ps1 | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index fea80ed..6376509 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -46,33 +46,6 @@ function Build-ALPackage { [String]$ALEXEPath ) - try { - if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - $alcPath = Join-Path -Path $ALEXEPath -ChildPath "win32" - $alcReference = Join-Path -Path $alcPath -ChildPath "ALC.EXE" - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - $alcPath = Join-Path -Path $ALEXEPath -ChildPath "win32" - $alcReference = Join-Path -Path $alcPath -ChildPath "ALC.EXE" - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - $alcPath = Join-Path -Path $ALEXEPath -ChildPath "linux" - $alcReference = Join-Path -Path $alcPath -ChildPath "ALC" - } else { - Write-Error "This is being run on an unsupported agent; exiting" - exit 1 - } - } - catch { - if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - throw "An error occurred; it doesn't seem that $alcPath contains a folder called 'win32', or the folder $alcPath\win32 doesn't contain ALC.EXE" - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - throw "An error occurred; it doesn't seem that $alcPath contains a folder called 'win32', or the folder $alcPath\win32 doesn't contain ALC.EXE" - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - throw "An error occurred; it doesn't seem that $alcPath contains a folder called 'linux', or the folder $alcPath\linux doesn't contain ALC" - } else { - throw "A different error has occurred: $($_.Exception.Message)" - } - } - if (-not (Test-Path -Path $alcReference)){ throw "ALC[.EXE] not found in $alcPath" } else { From c7afac33dae6f112dfe0e9ac9d395f6f8ad116c4 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 18:20:59 -0600 Subject: [PATCH 018/130] Missed a piece of code --- .../Build-ALPackage/function_Build-ALPackage.ps1 | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index 6376509..d3126d5 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -46,12 +46,6 @@ function Build-ALPackage { [String]$ALEXEPath ) - if (-not (Test-Path -Path $alcReference)){ - throw "ALC[.EXE] not found in $alcPath" - } else { - Write-Host "Found ALC[.EXE] at $alcPath" - } - if (-not (Test-Path -Path $PackagesDirectory)) { throw "Cannot find packages directory: $PackagesDirectory" } else { From aee7198062b738e3990ab77c4e31712fa5025fb0 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 18:34:31 -0600 Subject: [PATCH 019/130] Add guardrail for ALEXEPath --- .../Build-ALPackage/function_Build-ALPackage.ps1 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index d3126d5..336dc88 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -46,6 +46,19 @@ function Build-ALPackage { [String]$ALEXEPath ) + if (Test-Path -Path $ALEXEPath) { + $checkRef = Split-Path -Path $ALEXEPath -Leaf + if ($checkRef -eq "alc.exe" -or $checkRef -eq "alc") { + Write-Host "Confirmed existence of ALC[.EXE] at $ALEXEPath" + $alcReference = $ALEXEPath + } else { + Write-Error "Not sure what $ALEXEPath has, but the leaf is not (apparenlty) the compiler:" + Write-Error "ALEXEPath: $ALEXEPath" + Write-Error "Leaf: $checkRef" + exit 1 + } + } + if (-not (Test-Path -Path $PackagesDirectory)) { throw "Cannot find packages directory: $PackagesDirectory" } else { From adf84a023786f5e978d7cb46877c5ef52b130194 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 18:45:17 -0600 Subject: [PATCH 020/130] The guardrail didn't work? --- .../Build-ALPackage/function_Build-ALPackage.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index 336dc88..8ae6784 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -51,12 +51,17 @@ function Build-ALPackage { if ($checkRef -eq "alc.exe" -or $checkRef -eq "alc") { Write-Host "Confirmed existence of ALC[.EXE] at $ALEXEPath" $alcReference = $ALEXEPath + Write-Host "alcReference: $alcReference" } else { Write-Error "Not sure what $ALEXEPath has, but the leaf is not (apparenlty) the compiler:" Write-Error "ALEXEPath: $ALEXEPath" Write-Error "Leaf: $checkRef" exit 1 } + } else { + Write-Error "Having a problem with ALC[.EXE] location. Received '$ALEXEPath' but can't parse where the compiler is. Enumerating file system:" + Get-ChildItem -Path $(Build.SourcesDirectory)\*.* -Force -Recurse | %{$_.FullName} + exit 1 } if (-not (Test-Path -Path $PackagesDirectory)) { From f386cd367f88d2d59b9a332f2d6959c516ed6b6e Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 18:52:41 -0600 Subject: [PATCH 021/130] Again, why is pathing SO HARD for powershell? --- .../function_Build-ALPackage.ps1 | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index 8ae6784..27bd8b0 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -46,6 +46,23 @@ function Build-ALPackage { [String]$ALEXEPath ) + function ConvertFrom-DevopsPath { + param([Parameter(Mandatory)][string]$Path) + if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { + return [System.IO.Path]::GetFullPath($Path) + } else { + return $null + } + } + + Write-Host "Normalizing: $ALEXEPath" + $ALEXEPath = ConvertFrom-DevopsPath -Path $ALEXEPath + Write-Host "Normalized: $ALEXEPath" + if (Test-Path -Path $ALEXEPath) { $checkRef = Split-Path -Path $ALEXEPath -Leaf if ($checkRef -eq "alc.exe" -or $checkRef -eq "alc") { From d67876ac1379dd4652b8adaffa9fd141643b8eeb Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 19:18:59 -0600 Subject: [PATCH 022/130] Try a different resolution method? --- .../function_Build-ALPackage.ps1 | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index 27bd8b0..94a8ef9 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -46,24 +46,11 @@ function Build-ALPackage { [String]$ALEXEPath ) - function ConvertFrom-DevopsPath { - param([Parameter(Mandatory)][string]$Path) - if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - return [System.IO.Path]::GetFullPath($Path) - } else { - return $null - } - } - Write-Host "Normalizing: $ALEXEPath" - $ALEXEPath = ConvertFrom-DevopsPath -Path $ALEXEPath + $ALEXEPath = [System.IO.Path]::GetFullPath($ALEXEPath) Write-Host "Normalized: $ALEXEPath" - if (Test-Path -Path $ALEXEPath) { + if ((Test-Path -Path $ALEXEPath) -or ([System.IO.Path]::GetFullPath($ALEXEPath))) { $checkRef = Split-Path -Path $ALEXEPath -Leaf if ($checkRef -eq "alc.exe" -or $checkRef -eq "alc") { Write-Host "Confirmed existence of ALC[.EXE] at $ALEXEPath" From ce448f075a6fc2ef26e20330ed68fd54f37d92d9 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 19:28:31 -0600 Subject: [PATCH 023/130] Finding strange execution policies --- .../Build-ALPackage/function_Build-ALPackage.ps1 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index 94a8ef9..7acbe52 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -56,6 +56,19 @@ function Build-ALPackage { Write-Host "Confirmed existence of ALC[.EXE] at $ALEXEPath" $alcReference = $ALEXEPath Write-Host "alcReference: $alcReference" + + $expectedEnvPath = if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + Write-Host "Changing execution flag on $alcReference" + icacls $alcReference /grant Everyone:RX | Out-Null + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + Write-Host "Changing execution flag on $alcReference" + icacls $alcReference /grant Everyone:RX | Out-Null + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { + Write-Host "Changing execution flag on $alcReference" + chmod +x $alcReference + } } else { Write-Error "Not sure what $ALEXEPath has, but the leaf is not (apparenlty) the compiler:" Write-Error "ALEXEPath: $ALEXEPath" @@ -95,7 +108,7 @@ function Build-ALPackage { $OutputFile = Join-Path -Path $OutputDirectory -ChildPath $EntireAppName - & $alcReference /project:"$BaseProjectDirectory" /out:"$OutputFile" /packagecachepath:"$PackagesDirectory" + & "$alcReference" /project:"$BaseProjectDirectory" /out:"$OutputFile" /packagecachepath:"$PackagesDirectory" if ($LASTEXITCODE -ne 0) { throw "ALC compilation failed with exist code $LASTEXITCODE" From a8d7ab68783b20959f994d410d2c0ea96a1cf453 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 19:45:56 -0600 Subject: [PATCH 024/130] Apparently the file was coming down but strangely --- .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 561a150..4c0e954 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -103,7 +103,7 @@ function Get-VSIXCompilerVersion { Write-Host "Download target: $target" if (-not $DebugMode) { - Invoke-RestMethod -Uri $downloadUrl -Method Get -ContentType "application/json" -OutFile $target + Invoke-WebRequest -Uri $downloadUrl -OutFile $target -UseBasicParsing Write-Host "Downloaded file: $target" } @@ -114,10 +114,11 @@ function Get-VSIXCompilerVersion { Write-Host "Renamed '$target' to '$newFileName' for unzipping" - Write-Host "Extracting folder for '$platform' environment from VSIX" - $expandFolder = "expanded" + Get-Item $newPath | Format-List Name, Length, LastWriteTime + + Write-Host "Extracting folder for '$platform' environment from VSIX" try { Expand-Archive -Path $newPath -DestinationPath $expandFolder -Force } catch { @@ -125,6 +126,8 @@ function Get-VSIXCompilerVersion { exit 1 } + + # Step 5: Finish $expectedEnvPath = if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { "win32" From e965bdb8b1460c35b0a3f92a5f97d9b0ff75730a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 26 May 2025 20:03:36 -0600 Subject: [PATCH 025/130] So much ambiguity when I changed away from extreme manual execution --- .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 4c0e954..5688a90 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -114,11 +114,9 @@ function Get-VSIXCompilerVersion { Write-Host "Renamed '$target' to '$newFileName' for unzipping" - $expandFolder = "expanded" + $expandFolder = Join-Path -Path $DownloadDirectory -ChildPath "expanded" - Get-Item $newPath | Format-List Name, Length, LastWriteTime - - Write-Host "Extracting folder for '$platform' environment from VSIX" + Write-Host "Extracting folder for '$platform' environment from VSIX to '$expandFolder'" try { Expand-Archive -Path $newPath -DestinationPath $expandFolder -Force } catch { From 230f8861d6ef58e92c699284e79367035d1ab20a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 13:43:10 -0600 Subject: [PATCH 026/130] Removed deprecated function; added a couple lines of debugging --- .../function_Expand-Folder.ps1 | 63 ------------------- .../function_Get-VSIXCompiler.ps1 | 11 ++-- .../wrapper_Get-VSIXCompiler.ps1 | 1 - 3 files changed, 5 insertions(+), 70 deletions(-) delete mode 100644 bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 deleted file mode 100644 index 55a94dc..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/function_Expand-Folder.ps1 +++ /dev/null @@ -1,63 +0,0 @@ -function Expand-Folder { - param( - [Parameter(Mandatory)] - [String]$FileName, - [Parameter(Mandatory)] - [String]$ExtractFolder, - [Parameter()] - [String]$TopExtractedFolder = "expanded" - ) - - Add-Type -AssemblyName System.IO.Compression.FileSystem - - $extractionPath = Join-Path -Path (Split-Path $FileName) -ChildPath $TopExtractedFolder - $targetPrefix = if ($ExtractFolder.EndsWith("/")) { - $ExtractFolder - } else { - "$ExtractFolder/" - } - - Write-Host "Expanding '$FileName' folder '$targetPrefix' to '$extractionPath'" - - $fs = [System.IO.File]::OpenRead($FileName) - $zipCtor = [System.IO.Compression.ZipArchive].GetConstructor( - @([System.IO.Stream], [System.IO.Compression.ZipArchiveMode]) - ) - $zip = $zipCtor.Invoke(@($fs, [System.IO.Compression.ZipArchiveMode]::Read)) - - try { - $subfolder = Split-Path -Path $targetPrefix -Leaf - - foreach ($entry in $zip.Entries) { - if ($entry.FullName.StartsWith($targetPrefix)) { - - $relativePath = $entry.FullName.Substring($targetPrefix.Length) - - if (-not [string]::IsNullOrEmpty($relativePath)) { - $destpath = Join-Path -Path (Join-Path -Path $extractionPath -ChildPath $subfolder) -ChildPath $relativePath - - $destDir = Split-Path -Path $destPath -Parent - if (-not (Test-Path $destDir)) { - New-Item -ItemType Directory -Path $destDir -Force | Out-Null - } - Write-Host "Extracting file: $entry" - $outStream = [System.IO.File]::Create($destPath) - $entry.Open().CopyTo($outStream) - $outStream.Close() - } - } - } - } - catch { - Write-Error "Error: $($_.Exception.Message)" - exit 1 - } - finally { - $fs.Close() - } - - if ($IsLinux) { - Write-Host "Executing chmod to allow access for all of the extracted files" - chmod -R 755 $$TopExtractedFolder - } -} diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 5688a90..b7d1536 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -1,9 +1,9 @@ function Get-VSIXCompilerVersion { param( - [Parameter()] - [String]$Version = 'latest', - [Parameter()] - [String]$DownloadDirectory = ".", + [Parameter(Mandatory)] + [String]$Version, + [Parameter(Mandatory)] + [String]$DownloadDirectory, [Parameter()] [Switch]$DebugMode ) @@ -115,6 +115,7 @@ function Get-VSIXCompilerVersion { Write-Host "Renamed '$target' to '$newFileName' for unzipping" $expandFolder = Join-Path -Path $DownloadDirectory -ChildPath "expanded" + Write-Host "Created folder '$expandFolder'" Write-Host "Extracting folder for '$platform' environment from VSIX to '$expandFolder'" try { @@ -124,8 +125,6 @@ function Get-VSIXCompilerVersion { exit 1 } - - # Step 5: Finish $expectedEnvPath = if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { "win32" diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 index 9218ba3..4aa05f5 100644 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 @@ -12,7 +12,6 @@ function ConvertFrom-DevopsPath { } . "./function_Get-VSIXCompiler.ps1" -#. "./function_Expand-Folder.ps1" $localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require $localCompilerVersion = Get-VstsInput -Name 'Version' -Require From 76a4a9d6c5782d9dc06cdd21eb4675a07a9bd4d8 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 13:49:11 -0600 Subject: [PATCH 027/130] Add multiple debugging routines --- .../Get-BCDependencies/function_Get-BCDependencies.ps1 | 8 ++++++++ .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 index 73f9faf..315a00a 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 @@ -218,6 +218,14 @@ function Get-BCDependencies { chmod -R 644 $$TopExtractedFolder } + Write-Host "########################################################################################################################################" + Write-Host "Enumerating filesystem from '$targetPackageDirectory'" + Write-Host "########################################################################################################################################" + Write-Host "" + Get-ChildItem -Path "$targetPackageDirectory" -Force -Recurse | %{$_.FullName} + Write-Host "" + Write-Host "########################################################################################################################################" + ######################################################################################################################################## # Internal function here ######################################################################################################################################## diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index b7d1536..bf0cfa7 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -148,6 +148,14 @@ function Get-VSIXCompilerVersion { $ALEXEPath = Join-Path -Path $expandFolder -ChildPath (Join-Path -Path "extension" -ChildPath (Join-Path -Path "bin" -ChildPath (Join-Path -Path $expectedEnvPath -ChildPath $expectedCompilerName))) + Write-Host "########################################################################################################################################" + Write-Host "Enumerating filesystem from '$expandFolder'" + Write-Host "########################################################################################################################################" + Write-Host "" + Get-ChildItem -Path "$expandFolder" -Force -Recurse | %{$_.FullName} + Write-Host "" + Write-Host "########################################################################################################################################" + Write-Host "Testing destination: $ALEXEPath" if (Test-Path -Path $ALEXEPath) { Write-Host "Routine complete; ALC[.EXE] should be located at $ALEXEPath" @@ -160,6 +168,6 @@ function Get-VSIXCompilerVersion { } else { Write-Error "'$ALEXEPath' did not resolve to a correct location. Enumerating file system for reference:" Write-Host "" - Get-ChildItem -Path $(Build.SourcesDirectory)\*.* -Force -Recurse | %{$_.FullName} + Get-ChildItem -Path "$(Build.SourcesDirectory)" -Force -Recurse | %{$_.FullName} } } \ No newline at end of file From 4951c7a040d97a86b0d7d13910a0282a7f84fbf4 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 14:10:01 -0600 Subject: [PATCH 028/130] Aha! A missing directory creation. --- .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index bf0cfa7..cac43a0 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -115,7 +115,11 @@ function Get-VSIXCompilerVersion { Write-Host "Renamed '$target' to '$newFileName' for unzipping" $expandFolder = Join-Path -Path $DownloadDirectory -ChildPath "expanded" - Write-Host "Created folder '$expandFolder'" + + if (-not (Test-Path -Path $expandFolder)) { + Create-Item -ItemType Directory -Path $expandFolder + Write-Host "Created folder '$expandFolder'" + } Write-Host "Extracting folder for '$platform' environment from VSIX to '$expandFolder'" try { From 9abc5f09e48b569d1e75c749a1cf9956aa2e7064 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 14:20:46 -0600 Subject: [PATCH 029/130] I know too many programming languages... --- .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index cac43a0..609c48e 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -117,7 +117,7 @@ function Get-VSIXCompilerVersion { $expandFolder = Join-Path -Path $DownloadDirectory -ChildPath "expanded" if (-not (Test-Path -Path $expandFolder)) { - Create-Item -ItemType Directory -Path $expandFolder + New-Item -ItemType Directory -Path $expandFolder Write-Host "Created folder '$expandFolder'" } From 30f14fde57a980937e39267266f4b85aa16b07e5 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 14:34:47 -0600 Subject: [PATCH 030/130] More debugging - expand-archive is not working properly --- .../function_Get-VSIXCompiler.ps1 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 609c48e..8aea390 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -111,9 +111,23 @@ function Get-VSIXCompilerVersion { $newFileName = "compiler.zip" $newPath = Join-Path -Path (Split-Path $target) -ChildPath $newFileName Rename-Item -Path $target -NewName $newPath -Force - + Write-Host "Renamed '$target' to '$newFileName' for unzipping" + Write-Host "########################################################################################################################################" + Write-Host "Checking on .zip file status of flie '$newPath'" + Write-Host "########################################################################################################################################" + Write-Host "" + + $stream = [System.IO.File]::OpenRead($newPath) + $reader = [System.IO.Compression.ZipArchive]::new($stream, [System.IO.Compression.ZipArchiveMode]::Read) + $reader.Entries | ForEach-Object { $_.FullName } + $reader.Dispose() + $stream.Dispose() + + Write-Host "" + Write-Host "########################################################################################################################################" + $expandFolder = Join-Path -Path $DownloadDirectory -ChildPath "expanded" if (-not (Test-Path -Path $expandFolder)) { @@ -154,6 +168,7 @@ function Get-VSIXCompilerVersion { Write-Host "########################################################################################################################################" Write-Host "Enumerating filesystem from '$expandFolder'" + Write-Host "File size: $((Get-Item $newPath).Length)" Write-Host "########################################################################################################################################" Write-Host "" Get-ChildItem -Path "$expandFolder" -Force -Recurse | %{$_.FullName} From 70b50ae14ba62695f4f7f29c71a024bbcd163ca5 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 15:00:08 -0600 Subject: [PATCH 031/130] Change debugging concepts for platform agnosticism --- .../function_Get-VSIXCompiler.ps1 | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 8aea390..39a37b7 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -105,6 +105,8 @@ function Get-VSIXCompilerVersion { if (-not $DebugMode) { Invoke-WebRequest -Uri $downloadUrl -OutFile $target -UseBasicParsing Write-Host "Downloaded file: $target" + $originalHash = Get-FileHash -Path $target -Algorithm SHA256 + Write-Host "SHA256: $originalHash" } # Step 3: Rename file because Azure Pipelines' version of Expand-Archive is a little b**** @@ -115,15 +117,23 @@ function Get-VSIXCompilerVersion { Write-Host "Renamed '$target' to '$newFileName' for unzipping" Write-Host "########################################################################################################################################" - Write-Host "Checking on .zip file status of flie '$newPath'" + Write-Host "Checking on .zip file status of file '$newPath'" Write-Host "########################################################################################################################################" Write-Host "" - $stream = [System.IO.File]::OpenRead($newPath) - $reader = [System.IO.Compression.ZipArchive]::new($stream, [System.IO.Compression.ZipArchiveMode]::Read) - $reader.Entries | ForEach-Object { $_.FullName } - $reader.Dispose() - $stream.Dispose() + $fileSize = (Get-Item -Path $$newPath).Length + Write-Host "File size: $fileSize bytes" + + $fileHash = Get-FileHash -Path $newPath -Algorithm SHA256 + Write-Host "SHA256 [new]: $fileHash" + Write-Host "SHA256 [old]: $originalHash" + + $bytes = Get-Content -Path $newPath -Encoding Byte -TotalCount 4 + if ($bytes[0] -eq 0x50 -and $bytes[1] -eq 0x48) { + Write-Host "The file header adheres to a PK file header" + } else { + throw "The resulting file at $newPath does not appear to have a PK header" + } Write-Host "" Write-Host "########################################################################################################################################" From 3786a7dea8bdabacdb966476989be03d4c21df38 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 15:11:48 -0600 Subject: [PATCH 032/130] Added rename failure defense --- .../function_Get-VSIXCompiler.ps1 | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index 39a37b7..e8d834e 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -106,27 +106,35 @@ function Get-VSIXCompilerVersion { Invoke-WebRequest -Uri $downloadUrl -OutFile $target -UseBasicParsing Write-Host "Downloaded file: $target" $originalHash = Get-FileHash -Path $target -Algorithm SHA256 - Write-Host "SHA256: $originalHash" + Write-Host "SHA256: $originalHash.Hash" } # Step 3: Rename file because Azure Pipelines' version of Expand-Archive is a little b**** $newFileName = "compiler.zip" $newPath = Join-Path -Path (Split-Path $target) -ChildPath $newFileName Rename-Item -Path $target -NewName $newPath -Force - + Write-Host "Renamed '$target' to '$newFileName' for unzipping" + + if (-not (Test-Path -Path $newPath)) { + Write-Host "Hey! compiler.zip rename didn't work; I'll try copying it instead" + Copy-Item -Path $target -Destination $newPath -Force + Write-Host "I just copied from $target to $newPath" + } else { + Write-Host "Just confirmed there is a path (file) at $newPath" + } Write-Host "########################################################################################################################################" Write-Host "Checking on .zip file status of file '$newPath'" Write-Host "########################################################################################################################################" Write-Host "" - $fileSize = (Get-Item -Path $$newPath).Length + $fileSize = (Get-Item -Path $newPath).Length Write-Host "File size: $fileSize bytes" $fileHash = Get-FileHash -Path $newPath -Algorithm SHA256 - Write-Host "SHA256 [new]: $fileHash" - Write-Host "SHA256 [old]: $originalHash" + Write-Host "SHA256 [new]: $fileHash.Hash" + Write-Host "SHA256 [old]: $originalHash.Hash" $bytes = Get-Content -Path $newPath -Encoding Byte -TotalCount 4 if ($bytes[0] -eq 0x50 -and $bytes[1] -eq 0x48) { From fdafac67f0a7b1cbf5fc717c0fea872d980ca791 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 16:14:59 -0600 Subject: [PATCH 033/130] Frikkin' typos --- .../Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index e8d834e..ebeacfc 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -137,7 +137,7 @@ function Get-VSIXCompilerVersion { Write-Host "SHA256 [old]: $originalHash.Hash" $bytes = Get-Content -Path $newPath -Encoding Byte -TotalCount 4 - if ($bytes[0] -eq 0x50 -and $bytes[1] -eq 0x48) { + if ($bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B) { Write-Host "The file header adheres to a PK file header" } else { throw "The resulting file at $newPath does not appear to have a PK header" From a9214efeef3ff94baf536756c09b00e329dd8104 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 18:51:06 -0600 Subject: [PATCH 034/130] Fix debugging bits; improve logging; do we have a winner? --- .../function_Build-ALPackage.ps1 | 2 +- .../function_Get-BCDependencies.ps1 | 19 ++++------ .../function_Get-VSIXCompiler.ps1 | 38 ++++++++++--------- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 index 7acbe52..c5ff2dd 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 @@ -77,7 +77,7 @@ function Build-ALPackage { } } else { Write-Error "Having a problem with ALC[.EXE] location. Received '$ALEXEPath' but can't parse where the compiler is. Enumerating file system:" - Get-ChildItem -Path $(Build.SourcesDirectory)\*.* -Force -Recurse | %{$_.FullName} + Get-ChildItem -Path $expandFolder -Recurse | ForEach-Object { Write-Host $_.FullName } exit 1 } diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 index 315a00a..4389769 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 @@ -218,15 +218,12 @@ function Get-BCDependencies { chmod -R 644 $$TopExtractedFolder } - Write-Host "########################################################################################################################################" - Write-Host "Enumerating filesystem from '$targetPackageDirectory'" - Write-Host "########################################################################################################################################" - Write-Host "" - Get-ChildItem -Path "$targetPackageDirectory" -Force -Recurse | %{$_.FullName} - Write-Host "" - Write-Host "########################################################################################################################################" - - ######################################################################################################################################## - # Internal function here - ######################################################################################################################################## + # Debug section+ + Write-Debug "########################################################################################################################################" + Write-Debug "Enumerating filesystem from '$targetPackageDirectory'" + Write-Debug "########################################################################################################################################" + Write-Debug "" + Get-ChildItem -Path "$targetPackageDirectory" -Force -Recurse | ForEach-Object { Write-Debug $_.FullName } + Write-Debug "" + Write-Debug "########################################################################################################################################" } diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 index ebeacfc..928edde 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 @@ -106,7 +106,7 @@ function Get-VSIXCompilerVersion { Invoke-WebRequest -Uri $downloadUrl -OutFile $target -UseBasicParsing Write-Host "Downloaded file: $target" $originalHash = Get-FileHash -Path $target -Algorithm SHA256 - Write-Host "SHA256: $originalHash.Hash" + Write-Host "SHA256: $($originalHash.Hash)" } # Step 3: Rename file because Azure Pipelines' version of Expand-Archive is a little b**** @@ -133,8 +133,8 @@ function Get-VSIXCompilerVersion { Write-Host "File size: $fileSize bytes" $fileHash = Get-FileHash -Path $newPath -Algorithm SHA256 - Write-Host "SHA256 [new]: $fileHash.Hash" - Write-Host "SHA256 [old]: $originalHash.Hash" + Write-Host "SHA256 [new]: $($fileHash.Hash)" + Write-Host "SHA256 [old]: $($originalHash.Hash)" $bytes = Get-Content -Path $newPath -Encoding Byte -TotalCount 4 if ($bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B) { @@ -182,20 +182,24 @@ function Get-VSIXCompilerVersion { "alc" } - $ALEXEPath = Join-Path -Path $expandFolder -ChildPath (Join-Path -Path "extension" -ChildPath (Join-Path -Path "bin" -ChildPath (Join-Path -Path $expectedEnvPath -ChildPath $expectedCompilerName))) + $ALEXEPath = Join-Path -Path $expandFolder -ChildPath (Join-Path -Path "extension" -ChildPath (Join-Path -Path "bin" -ChildPath $expectedEnvPath)) - Write-Host "########################################################################################################################################" - Write-Host "Enumerating filesystem from '$expandFolder'" - Write-Host "File size: $((Get-Item $newPath).Length)" - Write-Host "########################################################################################################################################" - Write-Host "" - Get-ChildItem -Path "$expandFolder" -Force -Recurse | %{$_.FullName} - Write-Host "" - Write-Host "########################################################################################################################################" + $ActualALEXE = Join-Path -Path $ALEXEPath -ChildPath $expectedCompilerName + + #Debug section + Write-Debug "########################################################################################################################################" + Write-Debug "Enumerating filesystem from '$expandFolder'" + Write-Debug "File size: $((Get-Item $newPath).Length)" + Write-Debug "########################################################################################################################################" + Write-Debug "" + Get-ChildItem -Path "$expandFolder" -Force -Recurse | ForEach-Object { Write-Debug $_.FullName } + Write-Debug "" + Write-Debug "########################################################################################################################################" - Write-Host "Testing destination: $ALEXEPath" - if (Test-Path -Path $ALEXEPath) { - Write-Host "Routine complete; ALC[.EXE] should be located at $ALEXEPath" + #/Debug section + Write-Host "Testing destination: $ActualALEXE" + if (Test-Path -Path $ActualALEXE) { + Write-Host "Routine complete; ALC[.EXE] should be located at $ALEXEPath, called '$expectedCompilerName'" Write-Host "Returning ALEXEPath from to function call" return [PSCustomObject]@{ @@ -203,8 +207,8 @@ function Get-VSIXCompilerVersion { Version = $getVersion } } else { - Write-Error "'$ALEXEPath' did not resolve to a correct location. Enumerating file system for reference:" + Write-Error "'$ActualALEXE' did not resolve to a correct location. Enumerating file system for reference:" Write-Host "" - Get-ChildItem -Path "$(Build.SourcesDirectory)" -Force -Recurse | %{$_.FullName} + Get-ChildItem -Path "$(Build.SourcesDirectory)" -Force -Recurse | ForEach-Object { Write-Host $_.FullName } } } \ No newline at end of file From 17bc18b62abc049e3e0a5d0af593231f263bca5d Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 27 May 2025 19:38:18 -0600 Subject: [PATCH 035/130] Update environment tick and documentation --- _tasks/environments.json | 2 +- bc-tools-extension/README.md | 19 ++++++++++++- bc-tools-extension/RELEASE.md | 53 ++++++++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/_tasks/environments.json b/_tasks/environments.json index 59464ec..995f323 100644 --- a/_tasks/environments.json +++ b/_tasks/environments.json @@ -2,7 +2,7 @@ "version": { "major": 0, "minor": 1, - "patch": 3, + "patch": 4, "build": 0 }, "dev": { diff --git a/bc-tools-extension/README.md b/bc-tools-extension/README.md index a96a419..ed7d266 100644 --- a/bc-tools-extension/README.md +++ b/bc-tools-extension/README.md @@ -1,6 +1,21 @@ # Business Central Build Tasks for Azure DevOps + * [Overview](#overview) + * [Features](#features) + * [Installation](#installation) + * [Other Requirements](#other-requirements) + + [Azure AD App Registration](#azure-ad-app-registration) + + [Business Central Configuration](#business-central-configuration) + + [Setup Complete](#setup-complete) + * [Tasks Included](#tasks-included) + + [1. Get AL Compiler (`EGGetALCompiler`)](#1-get-al-compiler-eggetalcompiler) + + [2. Get AL Dependencies (`Get-BCDependencies`)](#2-get-al-dependencies-get-bcdependencies) + + [3. Build AL Package (`EGALBuildPackage`)](#3-build-al-package-egalbuildpackage) + * [Example Pipeline](#example-pipeline) + * [Security & Trust](#security--trust) + * [Support](#support) + ## Overview **WINDOWS AGENTS ONLY** @@ -136,6 +151,7 @@ You want to provide this user with the out-of-the-box permission set `EXTEN. MGT |Type|Name|Required|Default|Use| |---|---|---|---|---| |Input|`DownloadDirectory`|x|`$(Build.ArtifactStagingDirectory)`|The destination of the compiler; will expand into a folder called `expanded`| +|Input|`TargetVersion`| |`latest`|Use a **full** specific version if you need it, i.e. `16.0.1463980`, or to get the latest version, ignore this or put `latest`| |Output|`alVersion`||string|The version number of the extracted compiler| |Output|`alPath`||string|The path to the `expanded\extension\bin` folder that contains `win32\alc.exe`; used in later steps| @@ -143,7 +159,7 @@ You want to provide this user with the out-of-the-box permission set `EXTEN. MGT If not using the `alVersion` variable from above, the system places the expanded archive in the `$(DownloadDirectory)\expanded\extension\bin` folder. (Technically it then goes one level lower, to the `win32` folder.) -### 2. Get AL Dependencies (`Get-BCDependencies`) +### 2. Get AL Dependencies (`EGGetALDependencies`) |Type|Name|Required|Default|Use| |---|---|---|---|---| @@ -173,6 +189,7 @@ If not using the `alVersion` variable from above, the system places the expanded displayName: "Get AL compiler" inputs: DownloadDirectory: $(Build.SourcesDirectory)\compiler + Version: 'latest' - task: EGGetALDependencies@0 displayName: "Get AL dependencies" diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index 8849c5a..4df7ac7 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -1,6 +1,57 @@ # Release Notes - BCBuildTasks Extension -## Version: 0.1.0 +- [Version: 0.1.4](#version-014) + + [Improvement Release](#improvement-release) + * [New Features](#new-features) + + [1. **EGGetALCompiler**](#1-eggetalcompiler) + + [2. **EGGetALDependencies**](#2-eggetaldependencies) + + [3. **EGBuildALPackage**](#3-egbuildalpackage) + * [Notes & Requirements](#notes--requirements) +- [Version: 0.1.0](#version-010) + + [Initial Release](#initial-release) + * [New Features](#new-features-1) + + [1. **EGGetALCompiler**](#1-eggetalcompiler-1) + + [2. **EGGetALDependencies**](#2-eggetaldependencies-1) + + [3. **EGBuildALPackage**](#3-egbuildalpackage-1) + * [Notes & Requirements](#notes--requirements-1) + * [Example Pipeline Usage](#example-pipeline-usage) + * [Known Limitations](#known-limitations) + * [Support](#support) + +# Version: 0.1.4 + +**Release Date:** 2025-05-27 + +--- + +### Improvement Release + +The 0.1.4 version of this release made some background wiring changes that allow for specific version targeting when getting the compiler, and some improvements in logging. + +--- + +## New Features + +### 1. **EGGetALCompiler** + +* Allows for selective version download of AL compiler from Visual Studio Marketplace +* Expanded logging for diagnostic purposes + +### 2. **EGGetALDependencies** + +* Expanded logging for diagnostic purposes + +### 3. **EGBuildALPackage** + +* Expanded logging for diagnostic purposes + +## Notes & Requirements + +* All tasks are **PowerShell3-based** and rely on **VstsTaskSdk** +* Tasks must run on **Windows agents only** (not cross-platform) +* I have plans to migrate to an agent platform agnostic routine, but it requires a great deal of refactoring + +# Version: 0.1.0 **Release Date:** 2025-05-22 From 9913b666580b9881a81c41ea98e8f951d5abfdbb Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 29 May 2025 18:59:13 -0600 Subject: [PATCH 036/130] Add list of companies quick-hit Refers to #18 --- .../function_Get-ListOfCompanies.js | 86 +++++++++++++++++++ .../Get-ListOfCompanies/package-lock.json | 54 ++++++++++++ .../Get-ListOfCompanies/package.json | 5 ++ .../Get-ListOfCompanies/task.json | 57 ++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js create mode 100644 bc-tools-extension/Get-ListOfCompanies/package-lock.json create mode 100644 bc-tools-extension/Get-ListOfCompanies/package.json create mode 100644 bc-tools-extension/Get-ListOfCompanies/task.json diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js new file mode 100644 index 0000000..7ecde39 --- /dev/null +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -0,0 +1,86 @@ +let fetch; +try { + fetch = require('node-fetch'); +} catch (err) { + console.warn("'node-fetch' not found. Attempting to install..."); + const { execSync } = require('child_process'); + try { + execSync('npm install node-fetch@2 --silent --no-progress --no-audit --loglevel=error', { stdio: 'inherit' }); + fetch = require('node-fetch'); + } catch (installErr) { + console.error("Auto-install failed. Aborting."); + process.exit(1); + } +} + +const tenantId = process.env.INPUT_TENANTID; +const clientId = process.env.INPUT_CLIENTID; +const clientSecret = process.env.INPUT_CLIENTSECRET; +const environmentName = process.env.INPUT_ENVIRONMENTNAME; + +async function getToken() { + const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + + const params = new URLSearchParams(); + params.append('grant_type', 'client_credentials'); + params.append('client_id', clientId); + params.append('client_secret', clientSecret); + params.append('scope', 'https://api.businesscentral.dynamics.com/.default'); + + const body = params.toString(); + + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body + }); + + if (!response.ok) { + console.error('Failed to acquire token: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Authentication failed'); + } + + const data = await response.json(); + return data.access_token; +} + +async function getCompanies(token) { + const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/v2.0/companies`; + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + console.error('Failed to get companies: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Company list query failed'); + } + + const data = await response.json(); + return data.value; +} + +(async () => { + try { + const token = await getToken(); + const companies = await getCompanies(token); + + console.log('Companies:'); + companies.forEach((company, idx) => { + const name = company.name; + const id = company.id; + console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); + }); + } + catch (error) { + console.error('Error: ', error.message); + } +})(); \ No newline at end of file diff --git a/bc-tools-extension/Get-ListOfCompanies/package-lock.json b/bc-tools-extension/Get-ListOfCompanies/package-lock.json new file mode 100644 index 0000000..1fa25be --- /dev/null +++ b/bc-tools-extension/Get-ListOfCompanies/package-lock.json @@ -0,0 +1,54 @@ +{ + "name": "Get-ListOfCompanies", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/bc-tools-extension/Get-ListOfCompanies/package.json b/bc-tools-extension/Get-ListOfCompanies/package.json new file mode 100644 index 0000000..f513339 --- /dev/null +++ b/bc-tools-extension/Get-ListOfCompanies/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "node-fetch": "^2.7.0" + } +} diff --git a/bc-tools-extension/Get-ListOfCompanies/task.json b/bc-tools-extension/Get-ListOfCompanies/task.json new file mode 100644 index 0000000..d28ede0 --- /dev/null +++ b/bc-tools-extension/Get-ListOfCompanies/task.json @@ -0,0 +1,57 @@ +{ + "id": "0d4e6693-bdcb-47c0-a373-67a34549da07", + "name": "EGGetBCCompanies", + "friendlyName": "Get BC Companies", + "description": "Gets a list of the company objects in Business Central.", + "helpMarkDown": "Please open a GitHub issue at https://github.com/crazycga/bcdevopsextension/issues for queries or support.", + "category": "Build", + "author": "Evergrowth Consulting", + "version": { + "Major": 0, + "Minor": 1, + "Patch": 3 + }, + "instanceNameFormat": "Collect list of companies", + "inputs": [ + { + "name": "TenantId", + "type": "string", + "label": "Azure Tenant Id", + "defaultValue": "", + "required": true, + "helpMarkDown": "The Azure tenant id of the compiling tenant." + }, + { + "name": "EnvironmentName", + "type": "string", + "label": "BC Environment Name", + "defaultValue": "sandbox", + "required": false, + "helpMarkDown": "The Business Central environment name of the compiling environment." + }, + { + "name": "ClientId", + "type": "string", + "label": "Azure Client Id", + "defaultValue": "", + "required": true, + "helpMarkDown": "The Azure Entra client id for the process." + }, + { + "name": "ClientSecret", + "type": "string", + "label": "Azure Client Secret", + "defaultValue": "", + "required": true, + "helpMarkDown": "The Azure Entra client secret for the process." + } + ], + "execution": { + "Node16": { + "target": "function_Get-ListOfCompanies.js" + }, + "Node20_1": { + "target": "function_Get-ListOfCompanies.js" + } + } +} From 5b9fe64985935a1a1172e162bb4eec3a0336e4d2 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 29 May 2025 21:15:40 -0600 Subject: [PATCH 037/130] Added function to get list of modules, confirm module and abstracted common JS tools Refers to #18 --- .../function_Get-ListOfCompanies.js | 69 +-------- .../function_Get-ListOfModules.js | 33 ++++ .../Get-ListOfModules/task.json | 73 +++++++++ bc-tools-extension/_common/CommonTools.js | 144 ++++++++++++++++++ .../package-lock.json | 2 +- .../{Get-ListOfCompanies => }/package.json | 0 6 files changed, 254 insertions(+), 67 deletions(-) create mode 100644 bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js create mode 100644 bc-tools-extension/Get-ListOfModules/task.json create mode 100644 bc-tools-extension/_common/CommonTools.js rename bc-tools-extension/{Get-ListOfCompanies => }/package-lock.json (98%) rename bc-tools-extension/{Get-ListOfCompanies => }/package.json (100%) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 7ecde39..a1acab6 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -1,77 +1,14 @@ -let fetch; -try { - fetch = require('node-fetch'); -} catch (err) { - console.warn("'node-fetch' not found. Attempting to install..."); - const { execSync } = require('child_process'); - try { - execSync('npm install node-fetch@2 --silent --no-progress --no-audit --loglevel=error', { stdio: 'inherit' }); - fetch = require('node-fetch'); - } catch (installErr) { - console.error("Auto-install failed. Aborting."); - process.exit(1); - } -} +const commonTools = require('../_common/CommonTools'); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; const clientSecret = process.env.INPUT_CLIENTSECRET; const environmentName = process.env.INPUT_ENVIRONMENTNAME; -async function getToken() { - const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; - - const params = new URLSearchParams(); - params.append('grant_type', 'client_credentials'); - params.append('client_id', clientId); - params.append('client_secret', clientSecret); - params.append('scope', 'https://api.businesscentral.dynamics.com/.default'); - - const body = params.toString(); - - const response = await fetch(tokenUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body - }); - - if (!response.ok) { - console.error('Failed to acquire token: ', response.status); - const error = await response.text(); - console.error(error); - throw new Error('Authentication failed'); - } - - const data = await response.json(); - return data.access_token; -} - -async function getCompanies(token) { - const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/v2.0/companies`; - - const response = await fetch(apiUrl, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json' - } - }); - - if (!response.ok) { - console.error('Failed to get companies: ', response.status); - const error = await response.text(); - console.error(error); - throw new Error('Company list query failed'); - } - - const data = await response.json(); - return data.value; -} - (async () => { try { - const token = await getToken(); - const companies = await getCompanies(token); + const token = await commonTools.getToken(tenantId, clientId, clientSecret); + const companies = await commonTools.getCompanies(token, tenantId, environmentName); console.log('Companies:'); companies.forEach((company, idx) => { diff --git a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js new file mode 100644 index 0000000..b71204a --- /dev/null +++ b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js @@ -0,0 +1,33 @@ +const commonTools = require('../_common/CommonTools'); + +const tenantId = process.env.INPUT_TENANTID; +const clientId = process.env.INPUT_CLIENTID; +const clientSecret = process.env.INPUT_CLIENTSECRET; +const environmentName = process.env.INPUT_ENVIRONMENTNAME; +const companyId = process.env.INPUT_COMPANYID; +const moduleId = process.env.INPUT_MODULEID; +const excludeMicrosoft = process.env.INPUT_EXCLUDEMICROSOFT; + +(async () => { + try { + const token = await commonTools.getToken(tenantId, clientId, clientSecret); + const modules = await commonTools.getModules(token, tenantId, environmentName, companyId, moduleId, excludeMicrosoft); + + console.log('Modules:'); + modules.forEach((module, idx) => { + const name = module.name; + const id = module.id; + console.log(`${idx + 1}. ${module.displayName} (ID: ${module.id})`); + }); + + // this is the test for commonTools.confirmModule + // if (await commonTools.confirmModule(token, tenantId, environmentName, companyId, moduleId) === true) { + // console.log("Huzzah!"); + // } else { + // console.log("Boo!"); + // } + } + catch (error) { + console.error('Error: ', error.message); + } +})(); \ No newline at end of file diff --git a/bc-tools-extension/Get-ListOfModules/task.json b/bc-tools-extension/Get-ListOfModules/task.json new file mode 100644 index 0000000..1a4b8a5 --- /dev/null +++ b/bc-tools-extension/Get-ListOfModules/task.json @@ -0,0 +1,73 @@ +{ + "id": "4a9b312e-5e0d-4239-a254-3ac8808c3c73", + "name": "EGGetBCModules", + "friendlyName": "Get BC modules in a tenant", + "description": "Gets a list of the module objects in Business Central.", + "helpMarkDown": "Please open a GitHub issue at https://github.com/crazycga/bcdevopsextension/issues for queries or support.", + "category": "Build", + "author": "Evergrowth Consulting", + "version": { + "Major": 0, + "Minor": 1, + "Patch": 3 + }, + "instanceNameFormat": "Collect list of modules", + "inputs": [ + { + "name": "TenantId", + "type": "string", + "label": "Azure Tenant Id", + "defaultValue": "", + "required": true, + "helpMarkDown": "The Azure tenant id of the compiling tenant." + }, + { + "name": "EnvironmentName", + "type": "string", + "label": "BC Environment Name", + "defaultValue": "sandbox", + "required": false, + "helpMarkDown": "The Business Central environment name of the compiling environment." + }, + { + "name": "ClientId", + "type": "string", + "label": "Azure Client Id", + "defaultValue": "", + "required": true, + "helpMarkDown": "The Azure Entra client id for the process." + }, + { + "name": "ClientSecret", + "type": "string", + "label": "Azure Client Secret", + "defaultValue": "", + "required": true, + "helpMarkDown": "The Azure Entra client secret for the process." + }, + { + "name": "ModuleId", + "type": "string", + "label": "Module Id (a guid)", + "defaultValue": "", + "required": false, + "helpMarkDown": "The module id to search for in the list." + }, + { + "name": "ExcludeMicrosoft", + "type": "boolean", + "label": "Exclude Microsoft Modules", + "defaultValue": true, + "required": false, + "helpMarkDown": "This excludes Microsoft modules and will only show third-party modules in the list." + } + ], + "execution": { + "Node16": { + "target": "function_Get-ListOfModules.js" + }, + "Node20_1": { + "target": "function_Get-ListOfModules.js" + } + } +} diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js new file mode 100644 index 0000000..1335c67 --- /dev/null +++ b/bc-tools-extension/_common/CommonTools.js @@ -0,0 +1,144 @@ +const path = require('path'); + +let fetch; +try { + fetch = require('node-fetch'); +} catch (err) { + console.warn("'node-fetch' not found. Attempting to install..."); + const projectRoot = path.resolve(__dirname, '..'); + + const { execSync } = require('child_process'); + try { + execSync('npm install node-fetch@2 --no-progress --log-level=warning', { cwd: projectRoot, stdio: 'inherit' }); + fetch = require('node-fetch'); + } catch (installErr) { + console.error("Auto-install failed. Aborting."); + console.error(installErr); + process.exit(1); + } +} + +function parseBool(val) { + const trueVals = ['true', '1', 'yes', 'on']; + const falseVals = ['false', '0', 'no', 'off']; + + if (typeof val === 'boolean') return val; + if (typeof val === 'string') { + const normalized = val.trim().toLowerCase(); + if (trueVals.includes(normalized)) return true; + if (falseVals.includes(normalized)) return false; + } + return false; // because the lack of the variable implies it wasn't set; think Powerhsell switch +} + +async function getToken(tenantId, clientId, clientSecret) { + const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + + const params = new URLSearchParams(); + params.append('grant_type', 'client_credentials'); + params.append('client_id', clientId); + params.append('client_secret', clientSecret); + params.append('scope', 'https://api.businesscentral.dynamics.com/.default'); + + const body = params.toString(); + + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body + }); + + if (!response.ok) { + console.error('Failed to acquire token: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Authentication failed'); + } + + const data = await response.json(); + return data.access_token; +} + +async function getCompanies(token, tenantId, environmentName) { + const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/v2.0/companies`; + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + console.error('Failed to get companies: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Company list query failed'); + } + + const data = await response.json(); + return data.value; +} + +async function getModules(token, tenantId, environmentName, companyId, moduleId, excludeMicrosoft) { + let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensions`; + + const filters = []; + + if (moduleId && moduleId.trim() !== "") { + filters.push(`id eq ${moduleId}`); + } + + if (parseBool(excludeMicrosoft)) { + filters.push(`publisher ne 'Microsoft'`); + } + + if (filters.length > 0) { + apiUrl += `?$filter=${filters.join(" and ")}`; + } + + console.debug(`API: ${apiUrl}`); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + console.error('Failed to get modules: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Module list query failed'); + } + + const data = await response.json(); + return data.value; +} + +async function confirmModule(token, tenantId, environmentName, companyId, moduleId) { + + if (typeof moduleId !== 'string' || moduleId.trim() === "") { + throw new Error(`Module id is blank or missing. Module id was: ${moduleId}`); + } + + let checkValue = await getModules(token, tenantId, environmentName, companyId, moduleId); + + checkValue.forEach((module, idx) => { + const name = module.name; + const id = module.id; + console.debug(`**** ${idx + 1}. ${module.displayName} (ID: ${module.id})`); + }); + + return checkValue.some(m => m.id === moduleId); +} + +module.exports = { + getToken, + getCompanies, + getModules, + confirmModule +} \ No newline at end of file diff --git a/bc-tools-extension/Get-ListOfCompanies/package-lock.json b/bc-tools-extension/package-lock.json similarity index 98% rename from bc-tools-extension/Get-ListOfCompanies/package-lock.json rename to bc-tools-extension/package-lock.json index 1fa25be..ccc128b 100644 --- a/bc-tools-extension/Get-ListOfCompanies/package-lock.json +++ b/bc-tools-extension/package-lock.json @@ -1,5 +1,5 @@ { - "name": "Get-ListOfCompanies", + "name": "bc-tools-extension", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/bc-tools-extension/Get-ListOfCompanies/package.json b/bc-tools-extension/package.json similarity index 100% rename from bc-tools-extension/Get-ListOfCompanies/package.json rename to bc-tools-extension/package.json From 32d7f76de4f72b8a90240f1aa972dc2ee8f7f2ef Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Sun, 1 Jun 2025 15:41:47 -0600 Subject: [PATCH 038/130] Potential working version with new tools --- .../function_Get-ListOfModules.js | 11 +- .../function_Publish-BCModuleToTenant.js | 54 +++ .../Publish-BCModuleToTenant/task.json | 96 +++++ bc-tools-extension/_common/CommonTools.js | 382 +++++++++++++++++- bc-tools-extension/package-lock.json | 12 +- bc-tools-extension/package.json | 3 +- bc-tools-extension/test.js | 11 + 7 files changed, 551 insertions(+), 18 deletions(-) create mode 100644 bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js create mode 100644 bc-tools-extension/Publish-BCModuleToTenant/task.json create mode 100644 bc-tools-extension/test.js diff --git a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js index b71204a..6a95fce 100644 --- a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js +++ b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js @@ -5,6 +5,7 @@ const clientId = process.env.INPUT_CLIENTID; const clientSecret = process.env.INPUT_CLIENTSECRET; const environmentName = process.env.INPUT_ENVIRONMENTNAME; const companyId = process.env.INPUT_COMPANYID; + const moduleId = process.env.INPUT_MODULEID; const excludeMicrosoft = process.env.INPUT_EXCLUDEMICROSOFT; @@ -19,15 +20,7 @@ const excludeMicrosoft = process.env.INPUT_EXCLUDEMICROSOFT; const id = module.id; console.log(`${idx + 1}. ${module.displayName} (ID: ${module.id})`); }); - - // this is the test for commonTools.confirmModule - // if (await commonTools.confirmModule(token, tenantId, environmentName, companyId, moduleId) === true) { - // console.log("Huzzah!"); - // } else { - // console.log("Boo!"); - // } - } - catch (error) { + } catch (error) { console.error('Error: ', error.message); } })(); \ No newline at end of file diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js new file mode 100644 index 0000000..5d3a622 --- /dev/null +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -0,0 +1,54 @@ +const commonTools = require('../_common/CommonTools'); + +const tenantId = process.env.INPUT_TENANTID; +const clientId = process.env.INPUT_CLIENTID; +const clientSecret = process.env.INPUT_CLIENTSECRET; +const environmentName = process.env.INPUT_ENVIRONMENTNAME; +const companyId = process.env.INPUT_COMPANYID; +const filePath = process.env.INPUT_APPFILEPATH; +const skipPolling = commonTools.parseBool(process.env.INPUT_SKIPPOLLING); +const pollingFrequency = parseInt(process.env.INPUT_POLLINGFREQUENCY); +const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); + +(async () => { + + console.log("Calling deployment of module with the following parameters:"); + console.log(`TenantId: ${tenantId}`); + console.log(`EnvironmentName: ${environmentName}`); + console.log(`ClientId: ${cleintId}`); + console.log(`ClientSecret: 'Yeah, right, try again'`); + console.log(`CompanyId: ${companyId}`); + console.log(`AppFilePath: ${filePath}`); + console.log(`SkipPolling: ${skipPolling}`); + console.log(`PollingFrequency: ${pollingFrequency}`); + console.log(`MaxPollingTimeout: ${maxTimeout}`); + console.log(''); + + try { + console.log('********** getToken'); + const token = await commonTools.getToken(tenantId, clientId, clientSecret); + console.log('********** createInstallationBookmark'); + let test = await commonTools.createInstallationBookmark(token, tenantId, environmentName, companyId); + console.log(test); + let extId; + if (Array.isArray(test)) { + extId = test[0].systemId; + } else { + extId = test.systemId; + } + console.log('ExtId (the bookmark): ', extId); + console.log('********** uploadInstallationFile'); + let resulting = await commonTools.uploadInstallationFile(token, tenantId, environmentName, companyId, extId, filePath); + console.log(resulting ?? 'resulting succeeded'); + console.log('********** callNavUploadCommand'); + let callUpload = await commonTools.callNavUploadCommand(token, tenantId, environmentName, companyId, extId); + console.log('********** now awaiting response'); + if (!skipPolling) { + let responseCallback = await commonTools.waitForResponse(token, tenantId, environmentName, companyId, extId, pollingFrequency, maxTimeout); + } + console.log('********** done'); + } + catch (error) { + console.error('Error: ', error.message); + } +})(); \ No newline at end of file diff --git a/bc-tools-extension/Publish-BCModuleToTenant/task.json b/bc-tools-extension/Publish-BCModuleToTenant/task.json new file mode 100644 index 0000000..d6f350f --- /dev/null +++ b/bc-tools-extension/Publish-BCModuleToTenant/task.json @@ -0,0 +1,96 @@ +{ + "id": "def7c0a0-0d00-4f62-ae3f-7f084561e721", + "name": "EGDeployBCModule", + "friendlyName": "Deploy BC module to a tenant", + "description": "Deploys a per-tenant extension to a tenant in Business Central.", + "helpMarkDown": "Please open a GitHub issue at https://github.com/crazycga/bcdevopsextension/issues for queries or support.", + "category": "Build", + "author": "Evergrowth Consulting", + "version": { + "Major": 0, + "Minor": 1, + "Patch": 3 + }, + "instanceNameFormat": "Deploy module to tenant", + "inputs": [ + { + "name": "TenantId", + "type": "string", + "label": "Azure Tenant Id", + "defaultValue": "", + "required": true, + "helpMarkDown": "The Azure tenant id of the compiling tenant." + }, + { + "name": "EnvironmentName", + "type": "string", + "label": "BC Environment Name", + "defaultValue": "sandbox", + "required": false, + "helpMarkDown": "The Business Central environment name of the compiling environment." + }, + { + "name": "ClientId", + "type": "string", + "label": "Azure Client Id", + "defaultValue": "", + "required": true, + "helpMarkDown": "The Azure Entra client id for the process." + }, + { + "name": "ClientSecret", + "type": "string", + "label": "Azure Client Secret", + "defaultValue": "", + "required": true, + "helpMarkDown": "The Azure Entra client secret for the process." + }, + { + "name": "CompanyId", + "type": "string", + "label": "Company Id (a guid)", + "defaultValue": "", + "required": true, + "helpMarkDown": "The a company id for the (main) deployment; note that it will deploy to the WHOLE tenant." + }, + { + "name": "AppFilePath", + "type": "string", + "label": ".app file name and path", + "required": true, + "helpMarkDown": "The file name and path to the already-compiled .app file." + }, + { + "name": "SkipPolling", + "type": "boolean", + "label": "Skip waiting for deployment to complete; default: false", + "defaultValue": false, + "required": false, + "helpMarkDown": "Set to true to skip waiting for the deployment to complete; default: false." + }, + { + "name": "PollingFrequency", + "type": "integer", + "label": "Polling frequency (in SECONDS) of API (default: 10)", + "defaultValue": 10, + "required": false, + "helpMarkDown": "The number of seconds between polling checks to see if the module is deployed." + }, + { + "name": "MaxPollingTimeout", + "type": "integer", + "label": "Max. time (in SECONDS) to wait (default: 600)", + "defaultValue": 600, + "required": true, + "helpMarkDown": "The maximum time (in SECONDS) to wait for deployment to resolve." + } + ], + "execution": { + "Node16": { + "target": "function_Publish-BCModuleToTenant.js" + }, + "Node20_1": { + "target": "function_Publish-BCModuleToTenant.js" + } + } +} diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 1335c67..5f3301b 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -1,23 +1,66 @@ const path = require('path'); +const fs = require('fs/promises'); +const testMode = process.env.INPUT_TESTMODE; + +// using an environmental variable of TESTMODE = "true" will try and prevent specific JSON posts; it is a left over from initial testing +if (parseBool(testMode)) { + console.log(`Invocation received with TestMode: ${testMode}`); +} + +// this module uses undici for fetching specifically because the call to Microsoft.NAV.upload will return malformed and node-fetch can't parse it let fetch; try { - fetch = require('node-fetch'); -} catch (err) { - console.warn("'node-fetch' not found. Attempting to install..."); + fetch = require('undici').fetch; +} catch (_) { + console.warn("'undici' not found. Attempting to install..."); const projectRoot = path.resolve(__dirname, '..'); const { execSync } = require('child_process'); try { - execSync('npm install node-fetch@2 --no-progress --log-level=warning', { cwd: projectRoot, stdio: 'inherit' }); - fetch = require('node-fetch'); + execSync('npm install undici --no-progress --loglevel=warn', { + cwd: projectRoot, + stdio: 'inherit' + }); + fetch = require('undici').fetch; } catch (installErr) { - console.error("Auto-install failed. Aborting."); + console.error("Auto-install of 'undici' failed. Aborting."); console.error(installErr); process.exit(1); } } +/** + * An obfuscation routine to block the client_secret in token request bodies + * @param {string} body - the body of the token request, as a string + * @returns {string} A string that obfuscates client_secret + */ +function maskSecretInObject(body) { + const clone = { ...body }; + if (clone.client_secret) { + clone.client_secret = '****'; + } + return clone; +} + +/** + * An obfuscation routine to block the client_secret in token request bodies + * @param {URLSearchParams} params - the parameters used in a token request + * @returns {URLSearchParams} an object that obfuscates client_secret + */ +function maskSecretInParams(params) { + const clone = new URLSearchParams(params); + if (clone.has('client_secret')) { + clone.set('client_secret', ''); + } + return clone; +} + +/** + * A standardized string parser to use in a boolean condition + * @param {string} val - a value that is being enumerated for a boolean condition + * @returns {boolean} true if (val) is a variant of 'true', '1', 'yes', or 'on'; false if not, or missing + */ function parseBool(val) { const trueVals = ['true', '1', 'yes', 'on']; const falseVals = ['false', '0', 'no', 'off']; @@ -31,6 +74,13 @@ function parseBool(val) { return false; // because the lack of the variable implies it wasn't set; think Powerhsell switch } +/** + * Gets a bearer token from the Microsoft login + * @param {string} tenantId - a guid of the tenant id for the login + * @param {string} clientId - a guid of the client id for the login + * @param {string} clientSecret - a string for the client secret of the login + * @returns {string} a bearer token, if successful + */ async function getToken(tenantId, clientId, clientSecret) { const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; @@ -59,6 +109,13 @@ async function getToken(tenantId, clientId, clientSecret) { return data.access_token; } +/** + * Gets a list of the companies in Business Central + * @param {string} token - the bearer token that has been acquired + * @param {string} tenantId - a guid of the tenant id for the Business Central tenant + * @param {string} environmentName - a string of the environment name from the administration center in Business Central + * @returns {object} a Business Central object, list of companies + */ async function getCompanies(token, tenantId, environmentName) { const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/v2.0/companies`; @@ -81,6 +138,16 @@ async function getCompanies(token, tenantId, environmentName) { return data.value; } +/** + * Gets a list of the installed modules in the Business Central tenant + * @param {string} token - bearer token that has been acquired + * @param {string} tenantId - a guid of the tenant id for the Business Central tenant + * @param {string} environmentName - a string of the environment name from the administration center in Business Central + * @param {string} companyId - a guid of the company id being enumerated (use getCompanies() for a list) + * @param {string} moduleId - optional - restrict to just this one reference of a module for the list + * @param {boolean} excludeMicrosoft - optional - restrict the list to just non-Microsoft modules only + * @returns {object} a Business Central object, list of modules installed + */ async function getModules(token, tenantId, environmentName, companyId, moduleId, excludeMicrosoft) { let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensions`; @@ -136,9 +203,310 @@ async function confirmModule(token, tenantId, environmentName, companyId, module return checkValue.some(m => m.id === moduleId); } +// steps for uploading and publishing an extension. +// 1. create an extension upload bookmark +// 2. upload the .app file to the id of the bookmark +// 3. POST to Microsoft.NAV.upload +// 4. wait a thousand years for completion +// 5. Profit! + +/** + * Gets the installation status from the Business Central installation subsystem + * @param {string} token + * @param {string} tenantId - GUID format + * @param {string} environmentName + * @param {string} companyId - GUID format + * @param {string} [operationId] - Guid format - optional + * @returns {object?} if successful, a response object; status is at .status + */ +async function getInstallationStatus(token, tenantId, environmentName, companyId, operationId) { + let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionDeploymentStatus`; + console.debug('API (getInstallationStatus)', apiUrl); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + console.error('Failed to get extension deployments: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Extension deployment status query failed'); + } + + const data = await response.json(); + return data.value; +} + +/** + * Creates the placeholder for the installation update; will return the existing record if a duplicate exists + * @param {string} token + * @param {string} tenantId - GUID format + * @param {string} environmentName + * @param {string} companyId - GUID format + * @param {string} [schedule] - optional - one of "Current version", "Next major version", "Next minor version"; default: "Current version" + * @param {string} [syncMode] - optional - one of "Add" or "Force"; default: "Add" + * @returns {object?} if successful, a response object; the salient point is ".systemId" (a guid) + */ +async function createInstallationBookmark(token, tenantId, environmentName, companyId, schedule, syncMode) { + // + // ** internal note: when this routine gets record back from POST, it is a singleton, like this: + // { + // "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/$metadata#companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload/$entity", + // "@odata.etag": "W/\"JzE5OzEwNjIwNjAxNDc4ODMyMzQ1MzAxOzAwOyc=\"", + // "systemId": "fed6d3c7-a03d-f011-be59-000d3aefada9", + // "schedule": "Current_x0020_version", + // "schemaSyncMode": "Add", + // "extensionContent@odata.mediaEditLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent", + // "extensionContent@odata.mediaReadLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent" + // } + // + // HOWEVER, when executing a GET command, it comes back as an array, like this: + // { + // "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/$metadata#companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload", + // "value": [ + // { + // "@odata.etag": "W/\"JzE5OzEwNjIwNjAxNDc4ODMyMzQ1MzAxOzAwOyc=\"", + // "systemId": "fed6d3c7-a03d-f011-be59-000d3aefada9", + // "schedule": "Current_x0020_version", + // "schemaSyncMode": "Add", + // "extensionContent@odata.mediaEditLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent", + // "extensionContent@odata.mediaReadLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent" + // } + // ] + // } + // + // This means, when returning the POST, the return is object.systemId, when returning the GET, the return is object[0].systemId + + let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload`; + console.debug('API (createInstallationBookmark)', apiUrl); + + // per: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/resources/dynamics_extensionupload + if (!schedule || schedule.trim() === "") { + schedule = 'Current version'; + } + + if (!['Current version', 'Next minor version', 'Next major version'].includes(schedule) && schedule?.trim() !== "") { + throw new Error ('\'schedule\' must be one of: \'Current version\', \'Next minor version\', or \'Next major version\', or left blank'); + } + + // per: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/resources/dynamics_extensionupload + if (!syncMode || syncMode.trim() === "") { + syncMode = "Add"; + } + + if (!['Add', 'Force Sync'].includes(syncMode) && syncMode?.trim() !== "") { + throw new Error ('\'syncMode\' must be one of: \'Add\', or \'Force Sync\', or left blank'); + } + + const body = { + schedule: schedule, + schemaSyncMode: syncMode + }; + + const _debugBody = await JSON.stringify(body); + console.debug('Request body:'); + console.debug(_debugBody); + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + const status = response.status; + + let errorResponse; + try { + errorResponse = await response.json(); + } catch (e) { + const raw = await response.text(); + console.error('Non-JSON error response: ', raw); + throw new Error('Extension slot creation failed with unknown format'); + } + + console.error('BC API error response: ', JSON.stringify(errorResponse, null, 2)); + + if (status === 400 && errorResponse?.error?.code === "Internal_EntityWithSameKeyExists") { + console.warn('Extension upload already exists - retrieving existing record...'); + const secondResponse = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (secondResponse.ok) { + console.log('Found existing record; parsing and returning to routine'); + const secondValue = await secondResponse.json(); + if (Array.isArray(secondValue)) { + return secondValue.value[0]; // see internal note above + } else { + return secondValue.value; + } + } + } + + throw new Error('Extension slot creation query failed'); + } + + const data = await response.json(); + console.debug('(createInstallationBookmark) returning: '); + console.debug(data); + return data; +} + +/** + * + * @param {string} token + * @param {string} tenantId + * @param {string} environmentName + * @param {string} companyId + * @param {string} operationId + * @param {string} filePathAndName + * @returns {boolean} true if successful; false if not + */ +async function uploadInstallationFile(token, tenantId, environmentName, companyId, operationId, filePathAndName) { + const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/extensionContent`; + console.debug('API (uploadInstallationFile): ', apiUrl); + + try { + await fs.access(filePathAndName); + const fileBuffer = await fs.readFile(filePathAndName); + const response = await fetch(apiUrl, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/octet-stream', + 'If-Match': '*' + }, + body: fileBuffer + }); + + if (!response.ok) { + const error = await response.text(); + const errorCode = response.status; + console.error('Upload failed: status code: ', errorCode); + console.error(`Upload failed [${response.status}]: ${error}`); + throw new Error('File upload failed.'); + } + + console.log('Upload successful of:', filePathAndName, 'with a status code of:', response.status); + + if (response.status === 204) { + return true; + } else { + return false; + } + } + catch (err) { + if (err.code === 'ENOENT') { + console.warn('File not found: ', filePathAndName); + } else { + console.error('Unexpected error during upload: ', err); + } + throw err; + } +} + +async function callNavUploadCommand(token, tenantId, environmentName, companyId, operationId) { + const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/Microsoft.NAV.upload`; + console.debug('API (callNavUploadCommand): ', apiUrl); + + try { + console.log('Preparing to call Microsoft.NAV.upload'); + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json', + 'Accept-Encoding': 'gzip, deflate, br' // exclude 'br' + } + }); + + console.log('Call to Microsoft.NAV.upload successful? ¯\\_(ツ)_/¯ It\'s not like Microsoft tells us...'); + if (!response.ok) { + console.error('Failed to call Microsoft.NAV.upload for deployment: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Extension upload call query failed'); + } + + console.debug('Made call to Microsoft.NAV.upload; response code: ', response.status); + } catch (err) { + console.error('Error during call: ', err.name, err.message); + throw err; + } +} + +async function waitForResponse(token, tenantId, environmentName, companyId, operationId, waitTime, maxWaitTime) { + const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms * 1000)); + const startTimeStamp = Date.now(); + let currentTimeStamp = Date.now(); + + console.log(`Waiting an initial ${waitTime} seconds before polling...`); + + let manualBreak = false; + do { + await sleep(waitTime); + let thisCheck = await getInstallationStatus(token, tenantId, environmentName, companyId, operationId); + if (Array.isArray(thisCheck)) { + if (thisCheck.length === 0) { + console.log('Received blank array back on extension installation status check; breaking'); + console.log('(This usually means that the upload call failed, and/or there are no other upload records in this instance of Business Central.)'); + manualBreak = true; + } + else { + if (thisCheck[0].status !== 'InProgress') { + console.log(`Received status '${thisCheck[0].status}' response; breaking`); + manualBreak = true; + } else { + console.log(`Received status '${thisCheck[0].status}'; continuing to wait another ${waitTime} seconds`); + } + } + } + console.debug(Date.now(), ': checked progress, result:', thisCheck[0].status); + } while ((((currentTimeStamp - startTimeStamp) / 1000) < maxWaitTime) && !parseBool(manualBreak)); +} + +// THIS IS A LEFTOVER FROM TESTING; UNCOMMENT TO USE +// async function postTokenJson(url, { headers = {}, body = {} } = {}) { +// if (parseBool(testMode)) { +// console.log(`[Mock POST]: ${url}`); +// console.log('[Mock Headers]: ', headers); +// console.log('[Mock Body]: ', maskSecretInParams(body)); +// return { +// ok: true, +// json: async () => ({ systemId: "00000000-0000-0000-0000-000000000000", schedule: "Current_x0020_version", schemaSyncMode: "Add" }) +// }; +// } else { +// return await fetch(url, { +// method: 'POST', +// headers: headers, +// body: body +// }); +// } +// } + module.exports = { getToken, getCompanies, getModules, - confirmModule + confirmModule, + getInstallationStatus, + createInstallationBookmark, + uploadInstallationFile, + callNavUploadCommand, + waitForResponse } \ No newline at end of file diff --git a/bc-tools-extension/package-lock.json b/bc-tools-extension/package-lock.json index ccc128b..813afe5 100644 --- a/bc-tools-extension/package-lock.json +++ b/bc-tools-extension/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "dependencies": { - "node-fetch": "^2.7.0" + "node-fetch": "^2.7.0", + "undici": "^7.10.0" } }, "node_modules/node-fetch": { @@ -34,6 +35,15 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/undici": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/bc-tools-extension/package.json b/bc-tools-extension/package.json index f513339..088ae6c 100644 --- a/bc-tools-extension/package.json +++ b/bc-tools-extension/package.json @@ -1,5 +1,6 @@ { "dependencies": { - "node-fetch": "^2.7.0" + "node-fetch": "^2.7.0", + "undici": "^7.10.0" } } diff --git a/bc-tools-extension/test.js b/bc-tools-extension/test.js new file mode 100644 index 0000000..24be0c8 --- /dev/null +++ b/bc-tools-extension/test.js @@ -0,0 +1,11 @@ + const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms * 1000)); + const startTimeStamp = Date.now(); + let currentTimeStamp = Date.now(); + + console.log(`Waiting an initial 10 seconds before polling...`); + await sleep(10); + + currentTimeStamp = Date.now(); + let elapsed = (currentTimeStamp - startTimeStamp) / 1000; + console.log(elapsed, 'seconds passed'); + From 5154a7beb8a49559b791239fdcf4441569126c95 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 08:57:25 -0600 Subject: [PATCH 039/130] Check in change to extension to grab new items --- bc-tools-extension/vss-extension.json | 41 ++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/bc-tools-extension/vss-extension.json b/bc-tools-extension/vss-extension.json index 19e7fd5..78e730c 100644 --- a/bc-tools-extension/vss-extension.json +++ b/bc-tools-extension/vss-extension.json @@ -73,6 +73,15 @@ }, { "path": "Build-ALPackage" + }, + { + "path": "Get-ListOfCompanies" + }, + { + "path": "Get-ListOfModules" + }, + { + "path": "Publish-BCModuleToTenant" } ], "contributions": [ @@ -105,6 +114,36 @@ "properties": { "name": "Build-ALPackage" } - } + }, + { + "id": "Get-ListOfCompaniesTask", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "Get-ListOfCompanies" + } + }, + { + "id": "Get-ListOfModulesTask", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "Get-ListOfModules" + } + }, + { + "id": "Publish-BCModuleToTenantTask", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "Publish-BCModuleToTenant" + } + } ] } \ No newline at end of file From 98117daadf5ea73a0a96d495d3f1915da6646515 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 09:48:24 -0600 Subject: [PATCH 040/130] Add _common folder to extension --- bc-tools-extension/vss-extension.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bc-tools-extension/vss-extension.json b/bc-tools-extension/vss-extension.json index 78e730c..6c6225a 100644 --- a/bc-tools-extension/vss-extension.json +++ b/bc-tools-extension/vss-extension.json @@ -82,6 +82,9 @@ }, { "path": "Publish-BCModuleToTenant" + }, + { + "path": "_common" } ], "contributions": [ From 5d16b29440268d43db0570ece7356df287a4c251 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 09:59:54 -0600 Subject: [PATCH 041/130] Try moving location of source file --- .../Get-ListOfCompanies/function_Get-ListOfCompanies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index a1acab6..3cb781e 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -1,4 +1,4 @@ -const commonTools = require('../_common/CommonTools'); +const commonTools = require('_common/CommonTools'); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; From 14032213bb55c11796ef2c54ad832a9e71c0ddd3 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 10:08:53 -0600 Subject: [PATCH 042/130] See if enumerating the parent helps? --- .../function_Get-ListOfCompanies.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 3cb781e..e4720a4 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -1,3 +1,16 @@ +const fs = require('fs'); + +fs.readdir('..', (err, files) => { + if (err) { + console.error('Error reading directory: ', err); + return; + } + console.log('Files in parent directory:'); + files.forEach(file=> { + console.log(file); + }); +}); + const commonTools = require('_common/CommonTools'); const tenantId = process.env.INPUT_TENANTID; From ce6c402ca79add725d47fec08abc229f5ad68833 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 10:13:26 -0600 Subject: [PATCH 043/130] I NEED to see the internals of this --- .../function_Get-ListOfCompanies.js | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index e4720a4..973deca 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -1,16 +1,5 @@ const fs = require('fs'); -fs.readdir('..', (err, files) => { - if (err) { - console.error('Error reading directory: ', err); - return; - } - console.log('Files in parent directory:'); - files.forEach(file=> { - console.log(file); - }); -}); - const commonTools = require('_common/CommonTools'); const tenantId = process.env.INPUT_TENANTID; @@ -19,18 +8,28 @@ const clientSecret = process.env.INPUT_CLIENTSECRET; const environmentName = process.env.INPUT_ENVIRONMENTNAME; (async () => { - try { - const token = await commonTools.getToken(tenantId, clientId, clientSecret); - const companies = await commonTools.getCompanies(token, tenantId, environmentName); - - console.log('Companies:'); - companies.forEach((company, idx) => { - const name = company.name; - const id = company.id; - console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); + fs.readdir('..', (err, files) => { + if (err) { + console.error('Error reading directory: ', err); + return; + } + console.log('Files in parent directory:'); + files.forEach(file => { + console.log(file); }); - } - catch (error) { - console.error('Error: ', error.message); - } + }); + // try { + // const token = await commonTools.getToken(tenantId, clientId, clientSecret); + // const companies = await commonTools.getCompanies(token, tenantId, environmentName); + + // console.log('Companies:'); + // companies.forEach((company, idx) => { + // const name = company.name; + // const id = company.id; + // console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); + // }); + // } + // catch (error) { + // console.error('Error: ', error.message); + // } })(); \ No newline at end of file From d3a2b8e0d161291df83d7023064e091185a5ab49 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 10:30:29 -0600 Subject: [PATCH 044/130] Ugh; forgot to comment the required --- .../Get-ListOfCompanies/function_Get-ListOfCompanies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 973deca..6808cfc 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -1,6 +1,6 @@ const fs = require('fs'); -const commonTools = require('_common/CommonTools'); +//const commonTools = require('_common/CommonTools'); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; From f8ce7967e444f66dd7e7d1a76400ddf0694f4cb9 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 10:37:02 -0600 Subject: [PATCH 045/130] Still troublehsooting because I don't know where the system is putting my common tools --- .../function_Get-ListOfCompanies.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 6808cfc..d9bedb9 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -8,6 +8,7 @@ const clientSecret = process.env.INPUT_CLIENTSECRET; const environmentName = process.env.INPUT_ENVIRONMENTNAME; (async () => { + console.log('Enumerating parent:'); fs.readdir('..', (err, files) => { if (err) { console.error('Error reading directory: ', err); @@ -18,6 +19,20 @@ const environmentName = process.env.INPUT_ENVIRONMENTNAME; console.log(file); }); }); + + console.log('Enumerating current:'); + fs.readdir('.', (err, files) => { + if (err) { + console.error('Error reading directory: ', err); + return; + } + console.log('Files in parent directory:'); + files.forEach(file => { + console.log(file); + }); + }); + + // try { // const token = await commonTools.getToken(tenantId, clientId, clientSecret); // const companies = await commonTools.getCompanies(token, tenantId, environmentName); From f5804467a4e6ef59d4ef28d80da9af908865c24a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 10:47:41 -0600 Subject: [PATCH 046/130] Mass enumeration now --- .../function_Get-ListOfCompanies.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index d9bedb9..5a6bd40 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -1,4 +1,5 @@ const fs = require('fs'); +const path = require('path'); //const commonTools = require('_common/CommonTools'); @@ -8,6 +9,10 @@ const clientSecret = process.env.INPUT_CLIENTSECRET; const environmentName = process.env.INPUT_ENVIRONMENTNAME; (async () => { + console.log('process.cwd():', process.cwd()); // where the process was started + console.log('__dirname:', __dirname); // where the current script file resides + + console.log('Enumerating parent:'); fs.readdir('..', (err, files) => { if (err) { @@ -32,6 +37,28 @@ const environmentName = process.env.INPUT_ENVIRONMENTNAME; }); }); + let current = __dirname; + let depth = 5; + for (let i = 0; i < depth; i++) { + console.log(`\n📂 Contents of: ${current}`); + try { + const files = fs.readdirSync(current); + files.forEach(f => console.log(' -', f)); + } catch (e) { + console.error(` (Error reading ${current}: ${e.message})`); + } + current = path.resolve(current, '..'); + } + + console.log('**********************************************************'); + const expectedPath = path.resolve(__dirname, '../_common/CommonTools.js'); + console.log('👀 Trying to stat:', expectedPath); + try { + fs.statSync(expectedPath); + console.log('✅ Found commonTools at expected path'); + } catch { + console.log('❌ commonTools NOT found at expected path'); + } // try { // const token = await commonTools.getToken(tenantId, clientId, clientSecret); From 6107782cfd8fd09548581ee2f96190b2e025df83 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 11:08:16 -0600 Subject: [PATCH 047/130] You have to embed commonTools in EACH TASK? --- .../_common/CommonTools.js | 512 ++++++++++++++++++ .../_common/CommonTools.ps1 | 29 + 2 files changed, 541 insertions(+) create mode 100644 bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.js create mode 100644 bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.ps1 diff --git a/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.js b/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.js new file mode 100644 index 0000000..5f3301b --- /dev/null +++ b/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.js @@ -0,0 +1,512 @@ +const path = require('path'); +const fs = require('fs/promises'); + +const testMode = process.env.INPUT_TESTMODE; + +// using an environmental variable of TESTMODE = "true" will try and prevent specific JSON posts; it is a left over from initial testing +if (parseBool(testMode)) { + console.log(`Invocation received with TestMode: ${testMode}`); +} + +// this module uses undici for fetching specifically because the call to Microsoft.NAV.upload will return malformed and node-fetch can't parse it +let fetch; +try { + fetch = require('undici').fetch; +} catch (_) { + console.warn("'undici' not found. Attempting to install..."); + const projectRoot = path.resolve(__dirname, '..'); + + const { execSync } = require('child_process'); + try { + execSync('npm install undici --no-progress --loglevel=warn', { + cwd: projectRoot, + stdio: 'inherit' + }); + fetch = require('undici').fetch; + } catch (installErr) { + console.error("Auto-install of 'undici' failed. Aborting."); + console.error(installErr); + process.exit(1); + } +} + +/** + * An obfuscation routine to block the client_secret in token request bodies + * @param {string} body - the body of the token request, as a string + * @returns {string} A string that obfuscates client_secret + */ +function maskSecretInObject(body) { + const clone = { ...body }; + if (clone.client_secret) { + clone.client_secret = '****'; + } + return clone; +} + +/** + * An obfuscation routine to block the client_secret in token request bodies + * @param {URLSearchParams} params - the parameters used in a token request + * @returns {URLSearchParams} an object that obfuscates client_secret + */ +function maskSecretInParams(params) { + const clone = new URLSearchParams(params); + if (clone.has('client_secret')) { + clone.set('client_secret', ''); + } + return clone; +} + +/** + * A standardized string parser to use in a boolean condition + * @param {string} val - a value that is being enumerated for a boolean condition + * @returns {boolean} true if (val) is a variant of 'true', '1', 'yes', or 'on'; false if not, or missing + */ +function parseBool(val) { + const trueVals = ['true', '1', 'yes', 'on']; + const falseVals = ['false', '0', 'no', 'off']; + + if (typeof val === 'boolean') return val; + if (typeof val === 'string') { + const normalized = val.trim().toLowerCase(); + if (trueVals.includes(normalized)) return true; + if (falseVals.includes(normalized)) return false; + } + return false; // because the lack of the variable implies it wasn't set; think Powerhsell switch +} + +/** + * Gets a bearer token from the Microsoft login + * @param {string} tenantId - a guid of the tenant id for the login + * @param {string} clientId - a guid of the client id for the login + * @param {string} clientSecret - a string for the client secret of the login + * @returns {string} a bearer token, if successful + */ +async function getToken(tenantId, clientId, clientSecret) { + const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + + const params = new URLSearchParams(); + params.append('grant_type', 'client_credentials'); + params.append('client_id', clientId); + params.append('client_secret', clientSecret); + params.append('scope', 'https://api.businesscentral.dynamics.com/.default'); + + const body = params.toString(); + + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body + }); + + if (!response.ok) { + console.error('Failed to acquire token: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Authentication failed'); + } + + const data = await response.json(); + return data.access_token; +} + +/** + * Gets a list of the companies in Business Central + * @param {string} token - the bearer token that has been acquired + * @param {string} tenantId - a guid of the tenant id for the Business Central tenant + * @param {string} environmentName - a string of the environment name from the administration center in Business Central + * @returns {object} a Business Central object, list of companies + */ +async function getCompanies(token, tenantId, environmentName) { + const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/v2.0/companies`; + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + console.error('Failed to get companies: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Company list query failed'); + } + + const data = await response.json(); + return data.value; +} + +/** + * Gets a list of the installed modules in the Business Central tenant + * @param {string} token - bearer token that has been acquired + * @param {string} tenantId - a guid of the tenant id for the Business Central tenant + * @param {string} environmentName - a string of the environment name from the administration center in Business Central + * @param {string} companyId - a guid of the company id being enumerated (use getCompanies() for a list) + * @param {string} moduleId - optional - restrict to just this one reference of a module for the list + * @param {boolean} excludeMicrosoft - optional - restrict the list to just non-Microsoft modules only + * @returns {object} a Business Central object, list of modules installed + */ +async function getModules(token, tenantId, environmentName, companyId, moduleId, excludeMicrosoft) { + let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensions`; + + const filters = []; + + if (moduleId && moduleId.trim() !== "") { + filters.push(`id eq ${moduleId}`); + } + + if (parseBool(excludeMicrosoft)) { + filters.push(`publisher ne 'Microsoft'`); + } + + if (filters.length > 0) { + apiUrl += `?$filter=${filters.join(" and ")}`; + } + + console.debug(`API: ${apiUrl}`); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + console.error('Failed to get modules: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Module list query failed'); + } + + const data = await response.json(); + return data.value; +} + +async function confirmModule(token, tenantId, environmentName, companyId, moduleId) { + + if (typeof moduleId !== 'string' || moduleId.trim() === "") { + throw new Error(`Module id is blank or missing. Module id was: ${moduleId}`); + } + + let checkValue = await getModules(token, tenantId, environmentName, companyId, moduleId); + + checkValue.forEach((module, idx) => { + const name = module.name; + const id = module.id; + console.debug(`**** ${idx + 1}. ${module.displayName} (ID: ${module.id})`); + }); + + return checkValue.some(m => m.id === moduleId); +} + +// steps for uploading and publishing an extension. +// 1. create an extension upload bookmark +// 2. upload the .app file to the id of the bookmark +// 3. POST to Microsoft.NAV.upload +// 4. wait a thousand years for completion +// 5. Profit! + +/** + * Gets the installation status from the Business Central installation subsystem + * @param {string} token + * @param {string} tenantId - GUID format + * @param {string} environmentName + * @param {string} companyId - GUID format + * @param {string} [operationId] - Guid format - optional + * @returns {object?} if successful, a response object; status is at .status + */ +async function getInstallationStatus(token, tenantId, environmentName, companyId, operationId) { + let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionDeploymentStatus`; + console.debug('API (getInstallationStatus)', apiUrl); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + console.error('Failed to get extension deployments: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Extension deployment status query failed'); + } + + const data = await response.json(); + return data.value; +} + +/** + * Creates the placeholder for the installation update; will return the existing record if a duplicate exists + * @param {string} token + * @param {string} tenantId - GUID format + * @param {string} environmentName + * @param {string} companyId - GUID format + * @param {string} [schedule] - optional - one of "Current version", "Next major version", "Next minor version"; default: "Current version" + * @param {string} [syncMode] - optional - one of "Add" or "Force"; default: "Add" + * @returns {object?} if successful, a response object; the salient point is ".systemId" (a guid) + */ +async function createInstallationBookmark(token, tenantId, environmentName, companyId, schedule, syncMode) { + // + // ** internal note: when this routine gets record back from POST, it is a singleton, like this: + // { + // "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/$metadata#companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload/$entity", + // "@odata.etag": "W/\"JzE5OzEwNjIwNjAxNDc4ODMyMzQ1MzAxOzAwOyc=\"", + // "systemId": "fed6d3c7-a03d-f011-be59-000d3aefada9", + // "schedule": "Current_x0020_version", + // "schemaSyncMode": "Add", + // "extensionContent@odata.mediaEditLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent", + // "extensionContent@odata.mediaReadLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent" + // } + // + // HOWEVER, when executing a GET command, it comes back as an array, like this: + // { + // "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/$metadata#companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload", + // "value": [ + // { + // "@odata.etag": "W/\"JzE5OzEwNjIwNjAxNDc4ODMyMzQ1MzAxOzAwOyc=\"", + // "systemId": "fed6d3c7-a03d-f011-be59-000d3aefada9", + // "schedule": "Current_x0020_version", + // "schemaSyncMode": "Add", + // "extensionContent@odata.mediaEditLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent", + // "extensionContent@odata.mediaReadLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent" + // } + // ] + // } + // + // This means, when returning the POST, the return is object.systemId, when returning the GET, the return is object[0].systemId + + let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload`; + console.debug('API (createInstallationBookmark)', apiUrl); + + // per: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/resources/dynamics_extensionupload + if (!schedule || schedule.trim() === "") { + schedule = 'Current version'; + } + + if (!['Current version', 'Next minor version', 'Next major version'].includes(schedule) && schedule?.trim() !== "") { + throw new Error ('\'schedule\' must be one of: \'Current version\', \'Next minor version\', or \'Next major version\', or left blank'); + } + + // per: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/resources/dynamics_extensionupload + if (!syncMode || syncMode.trim() === "") { + syncMode = "Add"; + } + + if (!['Add', 'Force Sync'].includes(syncMode) && syncMode?.trim() !== "") { + throw new Error ('\'syncMode\' must be one of: \'Add\', or \'Force Sync\', or left blank'); + } + + const body = { + schedule: schedule, + schemaSyncMode: syncMode + }; + + const _debugBody = await JSON.stringify(body); + console.debug('Request body:'); + console.debug(_debugBody); + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + const status = response.status; + + let errorResponse; + try { + errorResponse = await response.json(); + } catch (e) { + const raw = await response.text(); + console.error('Non-JSON error response: ', raw); + throw new Error('Extension slot creation failed with unknown format'); + } + + console.error('BC API error response: ', JSON.stringify(errorResponse, null, 2)); + + if (status === 400 && errorResponse?.error?.code === "Internal_EntityWithSameKeyExists") { + console.warn('Extension upload already exists - retrieving existing record...'); + const secondResponse = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (secondResponse.ok) { + console.log('Found existing record; parsing and returning to routine'); + const secondValue = await secondResponse.json(); + if (Array.isArray(secondValue)) { + return secondValue.value[0]; // see internal note above + } else { + return secondValue.value; + } + } + } + + throw new Error('Extension slot creation query failed'); + } + + const data = await response.json(); + console.debug('(createInstallationBookmark) returning: '); + console.debug(data); + return data; +} + +/** + * + * @param {string} token + * @param {string} tenantId + * @param {string} environmentName + * @param {string} companyId + * @param {string} operationId + * @param {string} filePathAndName + * @returns {boolean} true if successful; false if not + */ +async function uploadInstallationFile(token, tenantId, environmentName, companyId, operationId, filePathAndName) { + const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/extensionContent`; + console.debug('API (uploadInstallationFile): ', apiUrl); + + try { + await fs.access(filePathAndName); + const fileBuffer = await fs.readFile(filePathAndName); + const response = await fetch(apiUrl, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/octet-stream', + 'If-Match': '*' + }, + body: fileBuffer + }); + + if (!response.ok) { + const error = await response.text(); + const errorCode = response.status; + console.error('Upload failed: status code: ', errorCode); + console.error(`Upload failed [${response.status}]: ${error}`); + throw new Error('File upload failed.'); + } + + console.log('Upload successful of:', filePathAndName, 'with a status code of:', response.status); + + if (response.status === 204) { + return true; + } else { + return false; + } + } + catch (err) { + if (err.code === 'ENOENT') { + console.warn('File not found: ', filePathAndName); + } else { + console.error('Unexpected error during upload: ', err); + } + throw err; + } +} + +async function callNavUploadCommand(token, tenantId, environmentName, companyId, operationId) { + const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/Microsoft.NAV.upload`; + console.debug('API (callNavUploadCommand): ', apiUrl); + + try { + console.log('Preparing to call Microsoft.NAV.upload'); + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json', + 'Accept-Encoding': 'gzip, deflate, br' // exclude 'br' + } + }); + + console.log('Call to Microsoft.NAV.upload successful? ¯\\_(ツ)_/¯ It\'s not like Microsoft tells us...'); + if (!response.ok) { + console.error('Failed to call Microsoft.NAV.upload for deployment: ', response.status); + const error = await response.text(); + console.error(error); + throw new Error('Extension upload call query failed'); + } + + console.debug('Made call to Microsoft.NAV.upload; response code: ', response.status); + } catch (err) { + console.error('Error during call: ', err.name, err.message); + throw err; + } +} + +async function waitForResponse(token, tenantId, environmentName, companyId, operationId, waitTime, maxWaitTime) { + const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms * 1000)); + const startTimeStamp = Date.now(); + let currentTimeStamp = Date.now(); + + console.log(`Waiting an initial ${waitTime} seconds before polling...`); + + let manualBreak = false; + do { + await sleep(waitTime); + let thisCheck = await getInstallationStatus(token, tenantId, environmentName, companyId, operationId); + if (Array.isArray(thisCheck)) { + if (thisCheck.length === 0) { + console.log('Received blank array back on extension installation status check; breaking'); + console.log('(This usually means that the upload call failed, and/or there are no other upload records in this instance of Business Central.)'); + manualBreak = true; + } + else { + if (thisCheck[0].status !== 'InProgress') { + console.log(`Received status '${thisCheck[0].status}' response; breaking`); + manualBreak = true; + } else { + console.log(`Received status '${thisCheck[0].status}'; continuing to wait another ${waitTime} seconds`); + } + } + } + console.debug(Date.now(), ': checked progress, result:', thisCheck[0].status); + } while ((((currentTimeStamp - startTimeStamp) / 1000) < maxWaitTime) && !parseBool(manualBreak)); +} + +// THIS IS A LEFTOVER FROM TESTING; UNCOMMENT TO USE +// async function postTokenJson(url, { headers = {}, body = {} } = {}) { +// if (parseBool(testMode)) { +// console.log(`[Mock POST]: ${url}`); +// console.log('[Mock Headers]: ', headers); +// console.log('[Mock Body]: ', maskSecretInParams(body)); +// return { +// ok: true, +// json: async () => ({ systemId: "00000000-0000-0000-0000-000000000000", schedule: "Current_x0020_version", schemaSyncMode: "Add" }) +// }; +// } else { +// return await fetch(url, { +// method: 'POST', +// headers: headers, +// body: body +// }); +// } +// } + +module.exports = { + getToken, + getCompanies, + getModules, + confirmModule, + getInstallationStatus, + createInstallationBookmark, + uploadInstallationFile, + callNavUploadCommand, + waitForResponse +} \ No newline at end of file diff --git a/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.ps1 b/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.ps1 new file mode 100644 index 0000000..b6aacad --- /dev/null +++ b/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.ps1 @@ -0,0 +1,29 @@ + +function ConvertFrom-DevopsPath { + param([Parameter(Mandatory)][string]$Path) + if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) + } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { + return [System.IO.Path]::GetFullPath($Path) + } else { + return $null + } +} + +function Get-OSEnvironment { + if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { + return "win32" + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + return "win" + } + elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { + return "linux" + } + else { + return "unknown" + } +} + From 3b398da1ad2d7536158dc7c615383e5690141884 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 13:15:18 -0600 Subject: [PATCH 048/130] Commit build script and remove dead soul from test variant --- .../_common/CommonTools.js | 512 ------------------ .../_common/CommonTools.ps1 | 29 - bc-tools-extension/_common/_build.ps1 | 37 ++ 3 files changed, 37 insertions(+), 541 deletions(-) delete mode 100644 bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.js delete mode 100644 bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.ps1 create mode 100644 bc-tools-extension/_common/_build.ps1 diff --git a/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.js b/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.js deleted file mode 100644 index 5f3301b..0000000 --- a/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.js +++ /dev/null @@ -1,512 +0,0 @@ -const path = require('path'); -const fs = require('fs/promises'); - -const testMode = process.env.INPUT_TESTMODE; - -// using an environmental variable of TESTMODE = "true" will try and prevent specific JSON posts; it is a left over from initial testing -if (parseBool(testMode)) { - console.log(`Invocation received with TestMode: ${testMode}`); -} - -// this module uses undici for fetching specifically because the call to Microsoft.NAV.upload will return malformed and node-fetch can't parse it -let fetch; -try { - fetch = require('undici').fetch; -} catch (_) { - console.warn("'undici' not found. Attempting to install..."); - const projectRoot = path.resolve(__dirname, '..'); - - const { execSync } = require('child_process'); - try { - execSync('npm install undici --no-progress --loglevel=warn', { - cwd: projectRoot, - stdio: 'inherit' - }); - fetch = require('undici').fetch; - } catch (installErr) { - console.error("Auto-install of 'undici' failed. Aborting."); - console.error(installErr); - process.exit(1); - } -} - -/** - * An obfuscation routine to block the client_secret in token request bodies - * @param {string} body - the body of the token request, as a string - * @returns {string} A string that obfuscates client_secret - */ -function maskSecretInObject(body) { - const clone = { ...body }; - if (clone.client_secret) { - clone.client_secret = '****'; - } - return clone; -} - -/** - * An obfuscation routine to block the client_secret in token request bodies - * @param {URLSearchParams} params - the parameters used in a token request - * @returns {URLSearchParams} an object that obfuscates client_secret - */ -function maskSecretInParams(params) { - const clone = new URLSearchParams(params); - if (clone.has('client_secret')) { - clone.set('client_secret', ''); - } - return clone; -} - -/** - * A standardized string parser to use in a boolean condition - * @param {string} val - a value that is being enumerated for a boolean condition - * @returns {boolean} true if (val) is a variant of 'true', '1', 'yes', or 'on'; false if not, or missing - */ -function parseBool(val) { - const trueVals = ['true', '1', 'yes', 'on']; - const falseVals = ['false', '0', 'no', 'off']; - - if (typeof val === 'boolean') return val; - if (typeof val === 'string') { - const normalized = val.trim().toLowerCase(); - if (trueVals.includes(normalized)) return true; - if (falseVals.includes(normalized)) return false; - } - return false; // because the lack of the variable implies it wasn't set; think Powerhsell switch -} - -/** - * Gets a bearer token from the Microsoft login - * @param {string} tenantId - a guid of the tenant id for the login - * @param {string} clientId - a guid of the client id for the login - * @param {string} clientSecret - a string for the client secret of the login - * @returns {string} a bearer token, if successful - */ -async function getToken(tenantId, clientId, clientSecret) { - const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; - - const params = new URLSearchParams(); - params.append('grant_type', 'client_credentials'); - params.append('client_id', clientId); - params.append('client_secret', clientSecret); - params.append('scope', 'https://api.businesscentral.dynamics.com/.default'); - - const body = params.toString(); - - const response = await fetch(tokenUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body - }); - - if (!response.ok) { - console.error('Failed to acquire token: ', response.status); - const error = await response.text(); - console.error(error); - throw new Error('Authentication failed'); - } - - const data = await response.json(); - return data.access_token; -} - -/** - * Gets a list of the companies in Business Central - * @param {string} token - the bearer token that has been acquired - * @param {string} tenantId - a guid of the tenant id for the Business Central tenant - * @param {string} environmentName - a string of the environment name from the administration center in Business Central - * @returns {object} a Business Central object, list of companies - */ -async function getCompanies(token, tenantId, environmentName) { - const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/v2.0/companies`; - - const response = await fetch(apiUrl, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json' - } - }); - - if (!response.ok) { - console.error('Failed to get companies: ', response.status); - const error = await response.text(); - console.error(error); - throw new Error('Company list query failed'); - } - - const data = await response.json(); - return data.value; -} - -/** - * Gets a list of the installed modules in the Business Central tenant - * @param {string} token - bearer token that has been acquired - * @param {string} tenantId - a guid of the tenant id for the Business Central tenant - * @param {string} environmentName - a string of the environment name from the administration center in Business Central - * @param {string} companyId - a guid of the company id being enumerated (use getCompanies() for a list) - * @param {string} moduleId - optional - restrict to just this one reference of a module for the list - * @param {boolean} excludeMicrosoft - optional - restrict the list to just non-Microsoft modules only - * @returns {object} a Business Central object, list of modules installed - */ -async function getModules(token, tenantId, environmentName, companyId, moduleId, excludeMicrosoft) { - let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensions`; - - const filters = []; - - if (moduleId && moduleId.trim() !== "") { - filters.push(`id eq ${moduleId}`); - } - - if (parseBool(excludeMicrosoft)) { - filters.push(`publisher ne 'Microsoft'`); - } - - if (filters.length > 0) { - apiUrl += `?$filter=${filters.join(" and ")}`; - } - - console.debug(`API: ${apiUrl}`); - - const response = await fetch(apiUrl, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json' - } - }); - - if (!response.ok) { - console.error('Failed to get modules: ', response.status); - const error = await response.text(); - console.error(error); - throw new Error('Module list query failed'); - } - - const data = await response.json(); - return data.value; -} - -async function confirmModule(token, tenantId, environmentName, companyId, moduleId) { - - if (typeof moduleId !== 'string' || moduleId.trim() === "") { - throw new Error(`Module id is blank or missing. Module id was: ${moduleId}`); - } - - let checkValue = await getModules(token, tenantId, environmentName, companyId, moduleId); - - checkValue.forEach((module, idx) => { - const name = module.name; - const id = module.id; - console.debug(`**** ${idx + 1}. ${module.displayName} (ID: ${module.id})`); - }); - - return checkValue.some(m => m.id === moduleId); -} - -// steps for uploading and publishing an extension. -// 1. create an extension upload bookmark -// 2. upload the .app file to the id of the bookmark -// 3. POST to Microsoft.NAV.upload -// 4. wait a thousand years for completion -// 5. Profit! - -/** - * Gets the installation status from the Business Central installation subsystem - * @param {string} token - * @param {string} tenantId - GUID format - * @param {string} environmentName - * @param {string} companyId - GUID format - * @param {string} [operationId] - Guid format - optional - * @returns {object?} if successful, a response object; status is at .status - */ -async function getInstallationStatus(token, tenantId, environmentName, companyId, operationId) { - let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionDeploymentStatus`; - console.debug('API (getInstallationStatus)', apiUrl); - - const response = await fetch(apiUrl, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json' - } - }); - - if (!response.ok) { - console.error('Failed to get extension deployments: ', response.status); - const error = await response.text(); - console.error(error); - throw new Error('Extension deployment status query failed'); - } - - const data = await response.json(); - return data.value; -} - -/** - * Creates the placeholder for the installation update; will return the existing record if a duplicate exists - * @param {string} token - * @param {string} tenantId - GUID format - * @param {string} environmentName - * @param {string} companyId - GUID format - * @param {string} [schedule] - optional - one of "Current version", "Next major version", "Next minor version"; default: "Current version" - * @param {string} [syncMode] - optional - one of "Add" or "Force"; default: "Add" - * @returns {object?} if successful, a response object; the salient point is ".systemId" (a guid) - */ -async function createInstallationBookmark(token, tenantId, environmentName, companyId, schedule, syncMode) { - // - // ** internal note: when this routine gets record back from POST, it is a singleton, like this: - // { - // "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/$metadata#companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload/$entity", - // "@odata.etag": "W/\"JzE5OzEwNjIwNjAxNDc4ODMyMzQ1MzAxOzAwOyc=\"", - // "systemId": "fed6d3c7-a03d-f011-be59-000d3aefada9", - // "schedule": "Current_x0020_version", - // "schemaSyncMode": "Add", - // "extensionContent@odata.mediaEditLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent", - // "extensionContent@odata.mediaReadLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent" - // } - // - // HOWEVER, when executing a GET command, it comes back as an array, like this: - // { - // "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/$metadata#companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload", - // "value": [ - // { - // "@odata.etag": "W/\"JzE5OzEwNjIwNjAxNDc4ODMyMzQ1MzAxOzAwOyc=\"", - // "systemId": "fed6d3c7-a03d-f011-be59-000d3aefada9", - // "schedule": "Current_x0020_version", - // "schemaSyncMode": "Add", - // "extensionContent@odata.mediaEditLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent", - // "extensionContent@odata.mediaReadLink": "https://api.businesscentral.dynamics.com/v2.0/1e8ccacd-46cd-46d6-909a-8b8c3da8220b/ussandbox/api/microsoft/automation/v2.0/companies(fb615954-ba2b-f011-9af4-6045bdc89d67)/extensionUpload(fed6d3c7-a03d-f011-be59-000d3aefada9)/extensionContent" - // } - // ] - // } - // - // This means, when returning the POST, the return is object.systemId, when returning the GET, the return is object[0].systemId - - let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload`; - console.debug('API (createInstallationBookmark)', apiUrl); - - // per: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/resources/dynamics_extensionupload - if (!schedule || schedule.trim() === "") { - schedule = 'Current version'; - } - - if (!['Current version', 'Next minor version', 'Next major version'].includes(schedule) && schedule?.trim() !== "") { - throw new Error ('\'schedule\' must be one of: \'Current version\', \'Next minor version\', or \'Next major version\', or left blank'); - } - - // per: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/resources/dynamics_extensionupload - if (!syncMode || syncMode.trim() === "") { - syncMode = "Add"; - } - - if (!['Add', 'Force Sync'].includes(syncMode) && syncMode?.trim() !== "") { - throw new Error ('\'syncMode\' must be one of: \'Add\', or \'Force Sync\', or left blank'); - } - - const body = { - schedule: schedule, - schemaSyncMode: syncMode - }; - - const _debugBody = await JSON.stringify(body); - console.debug('Request body:'); - console.debug(_debugBody); - - const response = await fetch(apiUrl, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify(body) - }); - - if (!response.ok) { - const status = response.status; - - let errorResponse; - try { - errorResponse = await response.json(); - } catch (e) { - const raw = await response.text(); - console.error('Non-JSON error response: ', raw); - throw new Error('Extension slot creation failed with unknown format'); - } - - console.error('BC API error response: ', JSON.stringify(errorResponse, null, 2)); - - if (status === 400 && errorResponse?.error?.code === "Internal_EntityWithSameKeyExists") { - console.warn('Extension upload already exists - retrieving existing record...'); - const secondResponse = await fetch(apiUrl, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - }); - - if (secondResponse.ok) { - console.log('Found existing record; parsing and returning to routine'); - const secondValue = await secondResponse.json(); - if (Array.isArray(secondValue)) { - return secondValue.value[0]; // see internal note above - } else { - return secondValue.value; - } - } - } - - throw new Error('Extension slot creation query failed'); - } - - const data = await response.json(); - console.debug('(createInstallationBookmark) returning: '); - console.debug(data); - return data; -} - -/** - * - * @param {string} token - * @param {string} tenantId - * @param {string} environmentName - * @param {string} companyId - * @param {string} operationId - * @param {string} filePathAndName - * @returns {boolean} true if successful; false if not - */ -async function uploadInstallationFile(token, tenantId, environmentName, companyId, operationId, filePathAndName) { - const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/extensionContent`; - console.debug('API (uploadInstallationFile): ', apiUrl); - - try { - await fs.access(filePathAndName); - const fileBuffer = await fs.readFile(filePathAndName); - const response = await fetch(apiUrl, { - method: 'PATCH', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/octet-stream', - 'If-Match': '*' - }, - body: fileBuffer - }); - - if (!response.ok) { - const error = await response.text(); - const errorCode = response.status; - console.error('Upload failed: status code: ', errorCode); - console.error(`Upload failed [${response.status}]: ${error}`); - throw new Error('File upload failed.'); - } - - console.log('Upload successful of:', filePathAndName, 'with a status code of:', response.status); - - if (response.status === 204) { - return true; - } else { - return false; - } - } - catch (err) { - if (err.code === 'ENOENT') { - console.warn('File not found: ', filePathAndName); - } else { - console.error('Unexpected error during upload: ', err); - } - throw err; - } -} - -async function callNavUploadCommand(token, tenantId, environmentName, companyId, operationId) { - const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/Microsoft.NAV.upload`; - console.debug('API (callNavUploadCommand): ', apiUrl); - - try { - console.log('Preparing to call Microsoft.NAV.upload'); - const response = await fetch(apiUrl, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json', - 'Accept-Encoding': 'gzip, deflate, br' // exclude 'br' - } - }); - - console.log('Call to Microsoft.NAV.upload successful? ¯\\_(ツ)_/¯ It\'s not like Microsoft tells us...'); - if (!response.ok) { - console.error('Failed to call Microsoft.NAV.upload for deployment: ', response.status); - const error = await response.text(); - console.error(error); - throw new Error('Extension upload call query failed'); - } - - console.debug('Made call to Microsoft.NAV.upload; response code: ', response.status); - } catch (err) { - console.error('Error during call: ', err.name, err.message); - throw err; - } -} - -async function waitForResponse(token, tenantId, environmentName, companyId, operationId, waitTime, maxWaitTime) { - const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms * 1000)); - const startTimeStamp = Date.now(); - let currentTimeStamp = Date.now(); - - console.log(`Waiting an initial ${waitTime} seconds before polling...`); - - let manualBreak = false; - do { - await sleep(waitTime); - let thisCheck = await getInstallationStatus(token, tenantId, environmentName, companyId, operationId); - if (Array.isArray(thisCheck)) { - if (thisCheck.length === 0) { - console.log('Received blank array back on extension installation status check; breaking'); - console.log('(This usually means that the upload call failed, and/or there are no other upload records in this instance of Business Central.)'); - manualBreak = true; - } - else { - if (thisCheck[0].status !== 'InProgress') { - console.log(`Received status '${thisCheck[0].status}' response; breaking`); - manualBreak = true; - } else { - console.log(`Received status '${thisCheck[0].status}'; continuing to wait another ${waitTime} seconds`); - } - } - } - console.debug(Date.now(), ': checked progress, result:', thisCheck[0].status); - } while ((((currentTimeStamp - startTimeStamp) / 1000) < maxWaitTime) && !parseBool(manualBreak)); -} - -// THIS IS A LEFTOVER FROM TESTING; UNCOMMENT TO USE -// async function postTokenJson(url, { headers = {}, body = {} } = {}) { -// if (parseBool(testMode)) { -// console.log(`[Mock POST]: ${url}`); -// console.log('[Mock Headers]: ', headers); -// console.log('[Mock Body]: ', maskSecretInParams(body)); -// return { -// ok: true, -// json: async () => ({ systemId: "00000000-0000-0000-0000-000000000000", schedule: "Current_x0020_version", schemaSyncMode: "Add" }) -// }; -// } else { -// return await fetch(url, { -// method: 'POST', -// headers: headers, -// body: body -// }); -// } -// } - -module.exports = { - getToken, - getCompanies, - getModules, - confirmModule, - getInstallationStatus, - createInstallationBookmark, - uploadInstallationFile, - callNavUploadCommand, - waitForResponse -} \ No newline at end of file diff --git a/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.ps1 b/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.ps1 deleted file mode 100644 index b6aacad..0000000 --- a/bc-tools-extension/Get-ListOfCompanies/_common/CommonTools.ps1 +++ /dev/null @@ -1,29 +0,0 @@ - -function ConvertFrom-DevopsPath { - param([Parameter(Mandatory)][string]$Path) - if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - return [System.IO.Path]::GetFullPath($Path) - } else { - return $null - } -} - -function Get-OSEnvironment { - if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - return "win32" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - return "win" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - return "linux" - } - else { - return "unknown" - } -} - diff --git a/bc-tools-extension/_common/_build.ps1 b/bc-tools-extension/_common/_build.ps1 new file mode 100644 index 0000000..1686e58 --- /dev/null +++ b/bc-tools-extension/_common/_build.ps1 @@ -0,0 +1,37 @@ +# this is required to copy the contents of the _common directory to the various subtask folders + +############################################################################################### +# copy _common to all subfolders where _common is required +############################################################################################### + +Set-Location -Path ".." + +Write-Host "Current working directory: $(Get-Location)" + +Write-Host "Copying _common contents to:" + +$paths = @( + "./Get-ListOfCompanies", + "./Get-ListOfModules", + "./Publish-BCModuleToTenant" +) + +foreach($path in $paths) { + Write-Host (" {0,20}" -f $path) + $destPath = Join-Path -Path $path -ChildPath "_common" + + Write-Host "DestPath: $destPath" + if (-not (Test-Path -Path $destPath)) { + New-Item -ItemType Directory -Path $destPath | Out-Null + Write-Host "Created path: $destPath" + } + #Copy-Item -Path "_common\*" -Destination "$destPath" -Recurse -Force + Get-ChildItem -Path "_common" | Where-Object { $_.Name -ne "_build.ps1" } | ForEach-Object { + Copy-Item -Path $_.FullName -Destination $destPath -Recurse -Force + } + Write-Host "Copied '_common' to $path" +} + +Write-Host "Copy complete" + +############################################################################################### From 36b431cd93419b8e52511ca8dccf15292806aeef Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 13:19:44 -0600 Subject: [PATCH 049/130] Correct mis-referenced common tools --- .../function_Get-ListOfCompanies.js | 117 ++++++++++-------- .../function_Get-ListOfModules.js | 2 +- .../function_Publish-BCModuleToTenant.js | 2 +- 3 files changed, 65 insertions(+), 56 deletions(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 5a6bd40..afa1659 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -1,77 +1,86 @@ const fs = require('fs'); const path = require('path'); -//const commonTools = require('_common/CommonTools'); +const commonTools = require('_common/CommonTools'); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; const clientSecret = process.env.INPUT_CLIENTSECRET; const environmentName = process.env.INPUT_ENVIRONMENTNAME; +const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMODE); (async () => { - console.log('process.cwd():', process.cwd()); // where the process was started - console.log('__dirname:', __dirname); // where the current script file resides + // extreme debug mode is specifically used to enumerate the environment in which the VSIX is installed; it is undocumented for a reason + // and should only be used as a demonstration of how the system actually extracts the VSIX files into the agents. It was using this + // tool that I discovered that _each task_ required a _copy_ of the "_common" directory + if (extremeDebugMode) { + console.log('process.cwd():', process.cwd()); // where the process was started + console.log('__dirname:', __dirname); // where the current script file resides - console.log('Enumerating parent:'); - fs.readdir('..', (err, files) => { - if (err) { - console.error('Error reading directory: ', err); - return; - } - console.log('Files in parent directory:'); - files.forEach(file => { - console.log(file); + + console.log('Enumerating parent:'); + fs.readdir('..', (err, files) => { + if (err) { + console.error('Error reading directory: ', err); + return; + } + console.log('Files in parent directory:'); + files.forEach(file => { + console.log(file); + }); }); - }); - console.log('Enumerating current:'); - fs.readdir('.', (err, files) => { - if (err) { - console.error('Error reading directory: ', err); - return; - } - console.log('Files in parent directory:'); - files.forEach(file => { - console.log(file); + console.log('Enumerating current:'); + fs.readdir('.', (err, files) => { + if (err) { + console.error('Error reading directory: ', err); + return; + } + console.log('Files in parent directory:'); + files.forEach(file => { + console.log(file); + }); }); - }); - let current = __dirname; - let depth = 5; - for (let i = 0; i < depth; i++) { - console.log(`\n📂 Contents of: ${current}`); + let current = __dirname; + let depth = 5; + for (let i = 0; i < depth; i++) { + console.log(`\n📂 Contents of: ${current}`); + try { + const files = fs.readdirSync(current); + files.forEach(f => console.log(' -', f)); + } catch (e) { + console.error(` (Error reading ${current}: ${e.message})`); + } + current = path.resolve(current, '..'); + } + + console.log('**********************************************************'); + const expectedPath = path.resolve(__dirname, '../_common/CommonTools.js'); + console.log('👀 Trying to stat:', expectedPath); try { - const files = fs.readdirSync(current); - files.forEach(f => console.log(' -', f)); - } catch (e) { - console.error(` (Error reading ${current}: ${e.message})`); + fs.statSync(expectedPath); + console.log('✅ Found commonTools at expected path'); + } catch { + console.log('❌ commonTools NOT found at expected path'); } - current = path.resolve(current, '..'); } - console.log('**********************************************************'); - const expectedPath = path.resolve(__dirname, '../_common/CommonTools.js'); - console.log('👀 Trying to stat:', expectedPath); + + // This is the actual "getCompanies" code try { - fs.statSync(expectedPath); - console.log('✅ Found commonTools at expected path'); - } catch { - console.log('❌ commonTools NOT found at expected path'); - } - - // try { - // const token = await commonTools.getToken(tenantId, clientId, clientSecret); - // const companies = await commonTools.getCompanies(token, tenantId, environmentName); + const token = await commonTools.getToken(tenantId, clientId, clientSecret); + const companies = await commonTools.getCompanies(token, tenantId, environmentName); - // console.log('Companies:'); - // companies.forEach((company, idx) => { - // const name = company.name; - // const id = company.id; - // console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); - // }); - // } - // catch (error) { - // console.error('Error: ', error.message); - // } + console.log('Companies:'); + companies.forEach((company, idx) => { + const name = company.name; + const id = company.id; + console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); + }); + } + catch (error) { + console.error('Error: ', error.message); + } })(); \ No newline at end of file diff --git a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js index 6a95fce..b7e24e0 100644 --- a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js +++ b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js @@ -1,4 +1,4 @@ -const commonTools = require('../_common/CommonTools'); +const commonTools = require('_common/CommonTools'); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index 5d3a622..fcd745e 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -1,4 +1,4 @@ -const commonTools = require('../_common/CommonTools'); +const commonTools = require('_common/CommonTools'); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; From 2dfb655503cf704929db65a7563eb4c05082bb58 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 13:26:13 -0600 Subject: [PATCH 050/130] Move _build.ps1 to correct location --- {bc-tools-extension/_common => _tasks}/_build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {bc-tools-extension/_common => _tasks}/_build.ps1 (96%) diff --git a/bc-tools-extension/_common/_build.ps1 b/_tasks/_build.ps1 similarity index 96% rename from bc-tools-extension/_common/_build.ps1 rename to _tasks/_build.ps1 index 1686e58..ba717fa 100644 --- a/bc-tools-extension/_common/_build.ps1 +++ b/_tasks/_build.ps1 @@ -4,7 +4,7 @@ # copy _common to all subfolders where _common is required ############################################################################################### -Set-Location -Path ".." +Set-Location -Path "../bc-tools-extension" Write-Host "Current working directory: $(Get-Location)" From 8aae1c11173c1376d35e568e3951e67e8175269d Mon Sep 17 00:00:00 2001 From: crazycga Date: Mon, 2 Jun 2025 13:26:45 -0600 Subject: [PATCH 051/130] Update mainbuild.yml Update build script to correctly inject _build.ps1 --- .github/workflows/mainbuild.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/mainbuild.yml b/.github/workflows/mainbuild.yml index 40219b5..c452100 100644 --- a/.github/workflows/mainbuild.yml +++ b/.github/workflows/mainbuild.yml @@ -68,6 +68,10 @@ jobs: shell: pwsh run: '_tasks/Initialize-BuildContext.ps1 -Environment $env:BUILD_TYPE -BuildNumber $env:GITHUB_RUN_NUMBER' + - name: Copy _common folder to all tasks that require it + shell: pwsh + run: '_tasks/_build.ps1' + - name: Compile Azure Devops Extension run: tfx extension create --manifest-globs vss-extension.json working-directory: bc-tools-extension From 8a19e386c7d2792ee31ebef85fffacd77743b685 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 14:04:26 -0600 Subject: [PATCH 052/130] Try changing reference path on _build script --- _tasks/_build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tasks/_build.ps1 b/_tasks/_build.ps1 index ba717fa..c60003a 100644 --- a/_tasks/_build.ps1 +++ b/_tasks/_build.ps1 @@ -4,7 +4,7 @@ # copy _common to all subfolders where _common is required ############################################################################################### -Set-Location -Path "../bc-tools-extension" +Set-Location -Path "./bc-tools-extension" Write-Host "Current working directory: $(Get-Location)" From 9228a02cc5f8c7741cafdb06ecef649bf35b3ecf Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 14:11:24 -0600 Subject: [PATCH 053/130] Forgot to update task.json with extreme debugging mode switch --- bc-tools-extension/Get-ListOfCompanies/task.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bc-tools-extension/Get-ListOfCompanies/task.json b/bc-tools-extension/Get-ListOfCompanies/task.json index d28ede0..9835423 100644 --- a/bc-tools-extension/Get-ListOfCompanies/task.json +++ b/bc-tools-extension/Get-ListOfCompanies/task.json @@ -44,6 +44,14 @@ "defaultValue": "", "required": true, "helpMarkDown": "The Azure Entra client secret for the process." + }, + { + "name": "ExtremeDebugMode", + "type": "boolean", + "label": "Extreme Debug Mode", + "defaultValue": false, + "required": false, + "helpMarkDown": "Undocumented extreme debugging mode used top assist in the creation of the VSIX." } ], "execution": { From 15597fb0e8695f32624c0541bee3b9ac81a7de3b Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 14:16:06 -0600 Subject: [PATCH 054/130] Need extreme debugging --- .../function_Get-ListOfCompanies.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index afa1659..91aefb5 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -1,7 +1,7 @@ const fs = require('fs'); const path = require('path'); -const commonTools = require('_common/CommonTools'); +//const commonTools = require('_common/CommonTools'); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; @@ -69,18 +69,18 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD // This is the actual "getCompanies" code - try { - const token = await commonTools.getToken(tenantId, clientId, clientSecret); - const companies = await commonTools.getCompanies(token, tenantId, environmentName); + // try { + // const token = await commonTools.getToken(tenantId, clientId, clientSecret); + // const companies = await commonTools.getCompanies(token, tenantId, environmentName); - console.log('Companies:'); - companies.forEach((company, idx) => { - const name = company.name; - const id = company.id; - console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); - }); - } - catch (error) { - console.error('Error: ', error.message); - } + // console.log('Companies:'); + // companies.forEach((company, idx) => { + // const name = company.name; + // const id = company.id; + // console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); + // }); + // } + // catch (error) { + // console.error('Error: ', error.message); + // } })(); \ No newline at end of file From 5bc71d77ea6ab748ca9cf9ea0559e91fc0300ffb Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 14:30:16 -0600 Subject: [PATCH 055/130] Seriously debugging now --- .../Get-ListOfCompanies/function_Get-ListOfCompanies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 91aefb5..9e0ea8e 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -7,7 +7,7 @@ const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; const clientSecret = process.env.INPUT_CLIENTSECRET; const environmentName = process.env.INPUT_ENVIRONMENTNAME; -const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMODE); +const extremeDebugMode = true; //commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMODE); (async () => { // extreme debug mode is specifically used to enumerate the environment in which the VSIX is installed; it is undocumented for a reason From 2b493fda1481e2d9f18b5db942c4f29eea458e0a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 14:36:09 -0600 Subject: [PATCH 056/130] Correct for proper internal pathing on scripts --- .../function_Get-ListOfCompanies.js | 32 +++++++++---------- .../function_Get-ListOfModules.js | 2 +- .../function_Publish-BCModuleToTenant.js | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 9e0ea8e..6841784 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -1,13 +1,13 @@ const fs = require('fs'); const path = require('path'); -//const commonTools = require('_common/CommonTools'); +const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; const clientSecret = process.env.INPUT_CLIENTSECRET; const environmentName = process.env.INPUT_ENVIRONMENTNAME; -const extremeDebugMode = true; //commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMODE); +const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMODE); (async () => { // extreme debug mode is specifically used to enumerate the environment in which the VSIX is installed; it is undocumented for a reason @@ -68,19 +68,19 @@ const extremeDebugMode = true; //commonTools.parseBool(process.env.INPUT_EXTREME } - // This is the actual "getCompanies" code - // try { - // const token = await commonTools.getToken(tenantId, clientId, clientSecret); - // const companies = await commonTools.getCompanies(token, tenantId, environmentName); + //This is the actual "getCompanies" code + try { + const token = await commonTools.getToken(tenantId, clientId, clientSecret); + const companies = await commonTools.getCompanies(token, tenantId, environmentName); - // console.log('Companies:'); - // companies.forEach((company, idx) => { - // const name = company.name; - // const id = company.id; - // console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); - // }); - // } - // catch (error) { - // console.error('Error: ', error.message); - // } + console.log('Companies:'); + companies.forEach((company, idx) => { + const name = company.name; + const id = company.id; + console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); + }); + } + catch (error) { + console.error('Error: ', error.message); + } })(); \ No newline at end of file diff --git a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js index b7e24e0..8c9c8e0 100644 --- a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js +++ b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js @@ -1,4 +1,4 @@ -const commonTools = require('_common/CommonTools'); +const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index fcd745e..cf95e67 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -1,4 +1,4 @@ -const commonTools = require('_common/CommonTools'); +const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; From 087faa859b5bf71f8bced74c5b53184f00c5d753 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 14:42:56 -0600 Subject: [PATCH 057/130] Expose parseBool for subfunctions to use --- .../Get-ListOfCompanies/function_Get-ListOfCompanies.js | 1 - bc-tools-extension/_common/CommonTools.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 6841784..1b2c9c8 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -18,7 +18,6 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD console.log('process.cwd():', process.cwd()); // where the process was started console.log('__dirname:', __dirname); // where the current script file resides - console.log('Enumerating parent:'); fs.readdir('..', (err, files) => { if (err) { diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 5f3301b..2dc229e 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -508,5 +508,6 @@ module.exports = { createInstallationBookmark, uploadInstallationFile, callNavUploadCommand, - waitForResponse + waitForResponse, + parseBool } \ No newline at end of file From a90b4d02b35484f9f5ea9ad76ab4f349cc67cecb Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 14:47:07 -0600 Subject: [PATCH 058/130] Forgot to add "path" module --- .../Get-ListOfModules/function_Get-ListOfModules.js | 1 + .../function_Publish-BCModuleToTenant.js | 1 + 2 files changed, 2 insertions(+) diff --git a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js index 8c9c8e0..a276017 100644 --- a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js +++ b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js @@ -1,3 +1,4 @@ +const path = require('path'); const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); const tenantId = process.env.INPUT_TENANTID; diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index cf95e67..f653b97 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -1,3 +1,4 @@ +const path = require('path'); const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); const tenantId = process.env.INPUT_TENANTID; From a67def0c8428d715ca9097b9407af731138d0941 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 15:14:57 -0600 Subject: [PATCH 059/130] Missed parameter on task.json --- bc-tools-extension/Get-ListOfModules/task.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bc-tools-extension/Get-ListOfModules/task.json b/bc-tools-extension/Get-ListOfModules/task.json index 1a4b8a5..3bc0fa0 100644 --- a/bc-tools-extension/Get-ListOfModules/task.json +++ b/bc-tools-extension/Get-ListOfModules/task.json @@ -45,6 +45,14 @@ "required": true, "helpMarkDown": "The Azure Entra client secret for the process." }, + { + "name": "CompanyId", + "type": "string", + "label": "Company Id", + "defaultValue": "", + "required": true, + "helpMarkDown": "A company (any company) id from the Business Central tenant; use EGGetBCCompanies to get a list of companies and ids." + }, { "name": "ModuleId", "type": "string", From fa8b0c2bcf4536aebe2ece72eceba111792d94d7 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 2 Jun 2025 15:26:33 -0600 Subject: [PATCH 060/130] Crashing typo; needs fixing --- .../function_Publish-BCModuleToTenant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index f653b97..de68d05 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -16,7 +16,7 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); console.log("Calling deployment of module with the following parameters:"); console.log(`TenantId: ${tenantId}`); console.log(`EnvironmentName: ${environmentName}`); - console.log(`ClientId: ${cleintId}`); + console.log(`ClientId: ${clientId}`); console.log(`ClientSecret: 'Yeah, right, try again'`); console.log(`CompanyId: ${companyId}`); console.log(`AppFilePath: ${filePath}`); From 4938a932197c0526ecb3a37c0e8050c366879b49 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 3 Jun 2025 09:19:15 -0600 Subject: [PATCH 061/130] Update documentation --- bc-tools-extension/README.md | 97 +++++++++++++++++++++++++++++++---- bc-tools-extension/RELEASE.md | 46 ++++++++++++++++- 2 files changed, 132 insertions(+), 11 deletions(-) diff --git a/bc-tools-extension/README.md b/bc-tools-extension/README.md index ed7d266..604e4d8 100644 --- a/bc-tools-extension/README.md +++ b/bc-tools-extension/README.md @@ -1,4 +1,5 @@ + # Business Central Build Tasks for Azure DevOps * [Overview](#overview) @@ -10,8 +11,11 @@ + [Setup Complete](#setup-complete) * [Tasks Included](#tasks-included) + [1. Get AL Compiler (`EGGetALCompiler`)](#1-get-al-compiler-eggetalcompiler) - + [2. Get AL Dependencies (`Get-BCDependencies`)](#2-get-al-dependencies-get-bcdependencies) + + [2. Get AL Dependencies (`EGGetALDependencies`)](#2-get-al-dependencies-eggetaldependencies) + [3. Build AL Package (`EGALBuildPackage`)](#3-build-al-package-egalbuildpackage) + + [4. Get List of Companies (`EGGetBCCompanies`)](#4-get-list-of-companies-eggetbccompanies) + + [5. Get List of Extensions (`EGGetBCModules`)](#5-get-list-of-extensions-eggetbcmodules) + + [6. Publish Extension to Business Central (`EGDeployBCModule`)](#6-publish-extension-to-business-central-egdeploybcmodule) * [Example Pipeline](#example-pipeline) * [Security & Trust](#security--trust) * [Support](#support) @@ -182,33 +186,81 @@ If not using the `alVersion` variable from above, the system places the expanded |Input|`PackageCachePath`| |`$(Build.SourcesDirectory)\.alpackages`|The folder containing the downloaded `.app` files for the project| |Input|`ALEXEPathFolder`|x| |The location of the `bin` folder that contains `win32\alc.exe`; also the output of `Get-VSIXCompiler`| +### 4. Get List of Companies (`EGGetBCCompanies`) + +|Type|Name|Required|Default|Use| +|---|---|---|---|---| +|Input|`TenantId`|x|N/A|The tenant id of the _target_ Business Central to enumerate| +|Input|`EnvironmentName`|x|N/A|The environment name from the Business Central administration console for the _target_ environment| +|Input|`ClientId`|x|N/A|The client id authorized to access the administrative API| +|Input|`ClientSecret`|x|N/A|The client secret for the client id authorized to access the administrative API| + +### 5. Get List of Extensions (`EGGetBCModules`) + +|Type|Name|Required|Default|Use| +|---|---|---|---|---| +|Input|`TenantId`|x|N/A|The tenant id of the _target_ Business Central to enumerate| +|Input|`EnvironmentName`|x|N/A|The environment name from the Business Central administration console for the _target_ environment| +|Input|`ClientId`|x|N/A|The client id authorized to access the extension deployment API| +|Input|`ClientSecret`|x|N/A|The client secret for the client id authorized to access the extension deployment API| +|Input|`CompanyId`|x|N/A|A company id (guid) is required to enumerate, however the response should be for the tenant; acquire a company id from `EGGetBCCompanies` above| +|Input|`ModuleId`||``|To restrict the list of extensions to a single extension (perhaps the one being deployed), set this field to the guid of the extension| +|Input|`ExcludeMicrosoft`||`false`|Set to `true` to return a list that doesn't include extensions published by Microsoft; useful with large lists of extensions| + +### 6. Publish Extension to Business Central (`EGDeployBCModule`) + +**This function is still somewhat experimental.** + +This function is intended to publish a compiled extension (`.app` file) to a Business Central tenant, and wait for a response. In its current form it has been demonstrated to create the extension upload and successfully call the publish routine (effectively code complete), however there are many external circumstances that may cause this step to fail. + +Overall, the flow of operations is: +1. Create an upload bookmark in the API +2. Upload the actual `.app` file +3. Call a specific routine in the API +4. Call the extension deployment status API until success + +There is not much more control that is provided and even the response codes from the API do not provide enough information to determine problems, diagnose issues or troubleshoot. + +|Type|Name|Required|Default|Use| +|---|---|---|---|---| +|Input|`TenantId`|x|N/A|The tenant id of the _target_ Business Central to enumerate| +|Input|`EnvironmentName`|x|N/A|The environment name from the Business Central administration console for the _target_ environment| +|Input|`ClientId`|x|N/A|The client id authorized to access the extension deployment API| +|Input|`ClientSecret`|x|N/A|The client secret for the client id authorized to access the extension deployment API| +|Input|`CompanyId`|x|N/A|A company id (guid) is required to enumerate, however the response should be for the tenant; acquire a company id from `EGGetBCCompanies` above| +|Input|`AppFilePath`|x|N/A|The full file name and path of the pre-compiled `.app` file| +|Input|`SkipPolling`||`false`|Set to `true` to skip polling; note that the pipeline will be subsequently _unaware_ of the status of the upload| +|Input|`PollingFrequency`||`10`|The number of **seconds** to wait between attempts to poll the extension deployment status for information after the upload| +|Input|`MaxPollingTimeout`||`600`|The maximum number of **seconds** to stop the pipeline to wait for the result of the deployment status; **note: use this value to prevent the pipeline from consuming too much time waiting for a response**| + ## Example Pipeline ```yaml - task: EGGetALCompiler@0 displayName: "Get AL compiler" inputs: - DownloadDirectory: $(Build.SourcesDirectory)\compiler + DownloadDirectory: $(Build.SourcesDirectory)/compiler Version: 'latest' + ExpansionDirectory: 'expanded' - task: EGGetALDependencies@0 displayName: "Get AL dependencies" inputs: - ClientId: "" - ClientSecret: "" - EnvironmentName: 'ussandbox' - PathToAppJson: $(Build.SourcesDirectory)\ClientPTE + ClientId: "" + ClientSecret: "" + EnvironmentName: '' + PathToAppJson: $(Build.SourcesDirectory)\CodeModule PathToPackagesDirectory: $(Build.SourcesDirectory)\.alpackages - TenantId: "" + TenantId: "" - task: EGBuildALPackage@0 displayName: "Compile AL package" inputs: - ALEXEPathFolder: $(Build.SourcesDirectory)\compiler\expanded\extension\bin\ + ALEXEPathFolder: $(Build.SourcesDirectory)/compiler/expanded/extension/bin/win32/alc.exe EntireAppName: "TestApp.1.1.1.app" OutAppFolder: $(Build.ArtifactStagingDirectory) PackageCachePath: $(Build.SourcesDirectory)\.alpackages - ProjectPath: $(Build.SourcesDirectory)\ClientPTE + ProjectPath: $(Build.SourcesDirectory)\CodeModule - task: PublishBuildArtifacts@1 displayName: "Publish artifact" @@ -216,6 +268,33 @@ If not using the `alVersion` variable from above, the system places the expanded ArtifactName: "drop" PathtoPublish: $(Build.ArtifactStagingDirectory) +- task: EGDeployBCModule@0 + displayName: "Deploy module to Business Central" + inputs: + TenantId: "" + EnvironmentName: "" + ClientId: "" + ClientSecret: "" + CompanyId: "" + AppFilePath: "$(Build.ArtifactStagingDirectory)\\TestApp.1.1.1.app" + +- task: EGGetBCCompanies@0 + displayName: "Getting list of companies" + inputs: + TenantId: "" + EnvironmentName: "" + ClientId: "" + ClientSecret: "" + +- task: EGGetBCModules@0 + displayName: "Getting list of modules installed" + inputs: + TenantId: "" + EnvironmentName: "" + ClientId: "" + ClientSecret: "" + CompanyId: "" + ``` ## Security & Trust diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index 4df7ac7..9c58e27 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -1,15 +1,21 @@ # Release Notes - BCBuildTasks Extension +- [Version: 0.1.5](#version-015) + + [Feature Release](#feature-release) + * [New Features](#new-features) + + [1. **EGGetBCCompanies**](#1-eggetbccompanies) + + [2. EGGetBCModules](#2-eggetbcmodules) + + [3. EGDeployBCModule](#3-egdeploybcmodule) - [Version: 0.1.4](#version-014) + [Improvement Release](#improvement-release) - * [New Features](#new-features) + * [New Features](#new-features-1) + [1. **EGGetALCompiler**](#1-eggetalcompiler) + [2. **EGGetALDependencies**](#2-eggetaldependencies) + [3. **EGBuildALPackage**](#3-egbuildalpackage) * [Notes & Requirements](#notes--requirements) - [Version: 0.1.0](#version-010) + [Initial Release](#initial-release) - * [New Features](#new-features-1) + * [New Features](#new-features-2) + [1. **EGGetALCompiler**](#1-eggetalcompiler-1) + [2. **EGGetALDependencies**](#2-eggetaldependencies-1) + [3. **EGBuildALPackage**](#3-egbuildalpackage-1) @@ -18,6 +24,42 @@ * [Known Limitations](#known-limitations) * [Support](#support) +# Version: 0.1.5 + +**Release Date:** TBD + +--- + +### Feature Release + +The 0.1.5 of this release has introduced some new features and functionality. Some of this functionality, specifically `EGDeployBCModule` is still somewhat experimental and subject to future changes. + +## New Features + +### 1. **EGGetBCCompanies** + +This will provide the user with a list of the Business Central companies in a tenant, along with their company id. A sample output: + +``` +Companies: +1. CRONUS USA, Inc. (ID: fb615954-ba2b-f011-9af4-6045bdc89d67) +2. My Company (ID: 6f52db6a-ba2b-f011-9af4-6045bdc89d67) +``` + +At the time of this release, this routine does not provide an output variable. + +### 2. EGGetBCModules + +This will provide the user with a list of the installed modules in a Business Central tenant. + +At the time of this release, this routine does not provide an output variable. + +### 3. EGDeployBCModule + +Please ensure that you see the documentation in the Visual Studio Marketplace page on this routine prior to use. + +This will attempt to publish an .app file into a tenant. **This routine is still largely experimental, and feedback is welcomed.** + # Version: 0.1.4 **Release Date:** 2025-05-27 From 860b1ce365d6fe2d8c4cca4ba73369f2887a12ac Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 3 Jun 2025 09:23:11 -0600 Subject: [PATCH 062/130] Incremented version number in tasks and top-end; updated repo-level readme --- README.md | 41 +----------------- bc-tools-extension/Build-ALPackage/task.json | 2 +- .../Get-BCDependencies/task.json | 2 +- .../Get-ListOfCompanies/task.json | 2 +- .../Get-ListOfModules/task.json | 2 +- bc-tools-extension/Get-VSIXCompiler/task.json | 2 +- .../Publish-BCModuleToTenant/task.json | 2 +- bc-tools-extension/overview.md | 43 +++++++++++++++++++ bc-tools-extension/vss-extension.json | 2 +- 9 files changed, 51 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 4f3a89b..a12abc9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Please see the README.md in bc-tools-extension: [README.md](./bc-tools-extension ## Superseded by Later Events -The problem indicated below has been superseded by events that came later. Specifically, the issue can be resolved by: +The problem indicated in the `overview.md` has been superseded by events that came later. Specifically, the `vss-extension.json` issue can be resolved by: 1. do not refer to the file in `contents` 1. do not add the file in `files` @@ -17,42 +17,3 @@ The problem indicated below has been superseded by events that came later. Spec "path": "README.md" }, ``` - -## SUPERSEDED: ⚠️ Azure DevOps Marketplace Bug: `overview.md` Upload Failure - -When publishing this extension to the [Azure DevOps Marketplace](https://marketplace.visualstudio.com/azuredevops), we encountered a **long-standing undocumented issue** with `tfx extension create`. - -Even if you: - -* Include `overview.md` in the root -* Set `"overview": "overview.md"` in `vss-extension.json` -* Add it to the `files[]` array with `"addressable": false` -* Use proper UTF-8 encoding, BOM-free - -...the CLI will still fail to inject the required `` element in `extension.vsixmanifest`. - -### 🔥 The Result - -Marketplace upload fails with: - -``` -Uploaded extension package is missing an 'overview.md' file which is a mandatory details asset. -``` - -### ✅ The Only Reliable Fix - -Manually add this to your `.vsix` after building: - -```xml - -``` - -To do that: - -1. Unzip the `.vsix` -2. Open `extension.vsixmanifest` -3. Add the `` line inside the `` block -4. Rezip the package and upload - -This is the only proven workaround as of May 2025. See full investigation and issue: -[https://github.com/microsoft/tfs-cli/issues/402](https://github.com/microsoft/tfs-cli/issues/402) diff --git a/bc-tools-extension/Build-ALPackage/task.json b/bc-tools-extension/Build-ALPackage/task.json index 8836e7b..972976b 100644 --- a/bc-tools-extension/Build-ALPackage/task.json +++ b/bc-tools-extension/Build-ALPackage/task.json @@ -9,7 +9,7 @@ "version": { "Major": 0, "Minor": 1, - "Patch": 3 + "Patch": 5 }, "instanceNameFormat": "Build AL Package from $(ProjectPath)", "inputs": [ diff --git a/bc-tools-extension/Get-BCDependencies/task.json b/bc-tools-extension/Get-BCDependencies/task.json index b7c335a..674467b 100644 --- a/bc-tools-extension/Get-BCDependencies/task.json +++ b/bc-tools-extension/Get-BCDependencies/task.json @@ -9,7 +9,7 @@ "version": { "Major": 0, "Minor": 1, - "Patch": 3 + "Patch": 5 }, "instanceNameFormat": "Collect dependencies for AL project", "inputs": [ diff --git a/bc-tools-extension/Get-ListOfCompanies/task.json b/bc-tools-extension/Get-ListOfCompanies/task.json index 9835423..fdb6aa0 100644 --- a/bc-tools-extension/Get-ListOfCompanies/task.json +++ b/bc-tools-extension/Get-ListOfCompanies/task.json @@ -9,7 +9,7 @@ "version": { "Major": 0, "Minor": 1, - "Patch": 3 + "Patch": 5 }, "instanceNameFormat": "Collect list of companies", "inputs": [ diff --git a/bc-tools-extension/Get-ListOfModules/task.json b/bc-tools-extension/Get-ListOfModules/task.json index 3bc0fa0..9dcbf53 100644 --- a/bc-tools-extension/Get-ListOfModules/task.json +++ b/bc-tools-extension/Get-ListOfModules/task.json @@ -9,7 +9,7 @@ "version": { "Major": 0, "Minor": 1, - "Patch": 3 + "Patch": 5 }, "instanceNameFormat": "Collect list of modules", "inputs": [ diff --git a/bc-tools-extension/Get-VSIXCompiler/task.json b/bc-tools-extension/Get-VSIXCompiler/task.json index 9d136eb..17de076 100644 --- a/bc-tools-extension/Get-VSIXCompiler/task.json +++ b/bc-tools-extension/Get-VSIXCompiler/task.json @@ -9,7 +9,7 @@ "version": { "Major": 0, "Minor": 1, - "Patch": 3 + "Patch": 5 }, "instanceNameFormat": "Acquire compiler for AL project", "inputs": [ diff --git a/bc-tools-extension/Publish-BCModuleToTenant/task.json b/bc-tools-extension/Publish-BCModuleToTenant/task.json index d6f350f..95b5e62 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/task.json +++ b/bc-tools-extension/Publish-BCModuleToTenant/task.json @@ -9,7 +9,7 @@ "version": { "Major": 0, "Minor": 1, - "Patch": 3 + "Patch": 5 }, "instanceNameFormat": "Deploy module to tenant", "inputs": [ diff --git a/bc-tools-extension/overview.md b/bc-tools-extension/overview.md index 422ee7c..86b93c0 100644 --- a/bc-tools-extension/overview.md +++ b/bc-tools-extension/overview.md @@ -8,3 +8,46 @@ This Azure DevOps extension provides build pipeline tasks for Microsoft Dynamics **This extension is only usable on Windows-based agents** +------------------------------------- + +# Information take from original _repository_ README + + +## SUPERSEDED: ⚠️ Azure DevOps Marketplace Bug: `overview.md` Upload Failure + +When publishing this extension to the [Azure DevOps Marketplace](https://marketplace.visualstudio.com/azuredevops), we encountered a **long-standing undocumented issue** with `tfx extension create`. + +Even if you: + +* Include `overview.md` in the root +* Set `"overview": "overview.md"` in `vss-extension.json` +* Add it to the `files[]` array with `"addressable": false` +* Use proper UTF-8 encoding, BOM-free + +...the CLI will still fail to inject the required `` element in `extension.vsixmanifest`. + +### 🔥 The Result + +Marketplace upload fails with: + +``` +Uploaded extension package is missing an 'overview.md' file which is a mandatory details asset. +``` + +### ✅ The Only Reliable Fix + +Manually add this to your `.vsix` after building: + +```xml + +``` + +To do that: + +1. Unzip the `.vsix` +2. Open `extension.vsixmanifest` +3. Add the `` line inside the `` block +4. Rezip the package and upload + +This is the only proven workaround as of May 2025. See full investigation and issue: +[https://github.com/microsoft/tfs-cli/issues/402](https://github.com/microsoft/tfs-cli/issues/402) diff --git a/bc-tools-extension/vss-extension.json b/bc-tools-extension/vss-extension.json index 6c6225a..a9d7afb 100644 --- a/bc-tools-extension/vss-extension.json +++ b/bc-tools-extension/vss-extension.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "id": "eg-bc-build-tasks", "name": "Business Central Build Tasks", - "version": "0.1.3", + "version": "0.1.5", "publisher": "Evergrowth", "targets": [ { From 41d3d206049cd26cdf4a6b68b4a9e7fe5c3be649 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 3 Jun 2025 09:26:23 -0600 Subject: [PATCH 063/130] Correct release date --- bc-tools-extension/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index 9c58e27..349bd38 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -26,7 +26,7 @@ # Version: 0.1.5 -**Release Date:** TBD +**Release Date:** 2025-06-03 --- From 7c0b09c396e368ac92c9ca8c48f66cdb99fc5f14 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 3 Jun 2025 09:44:22 -0600 Subject: [PATCH 064/130] Correct tagging to only fire on dev-trunk and main --- .github/workflows/mainbuild.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mainbuild.yml b/.github/workflows/mainbuild.yml index c452100..d02c8b3 100644 --- a/.github/workflows/mainbuild.yml +++ b/.github/workflows/mainbuild.yml @@ -106,7 +106,7 @@ jobs: ADO_PAT: ${{ secrets.ADO_PAT }} - name: Tag repo with new version - if: env.SHOULD_PUBLISH == 'true' + if: env.SHOULD_PUBLISH == 'true' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev-trunk') shell: pwsh run: | $versionPath = "_tasks/environments.json" From e37c699c48f03887eac7301bf156c7a60c6f377a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 3 Jun 2025 09:48:13 -0600 Subject: [PATCH 065/130] Increment version in environments.json --- _tasks/environments.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tasks/environments.json b/_tasks/environments.json index 995f323..395784a 100644 --- a/_tasks/environments.json +++ b/_tasks/environments.json @@ -2,7 +2,7 @@ "version": { "major": 0, "minor": 1, - "patch": 4, + "patch": 5, "build": 0 }, "dev": { From 7f4a6df46d060dbeb08a6203526220a59d51d846 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 3 Jun 2025 10:05:10 -0600 Subject: [PATCH 066/130] I can't believe I missed this --- .github/workflows/mainbuild.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mainbuild.yml b/.github/workflows/mainbuild.yml index d02c8b3..df469e3 100644 --- a/.github/workflows/mainbuild.yml +++ b/.github/workflows/mainbuild.yml @@ -4,7 +4,7 @@ on: push: branches: - main - - dev-trunk + - dev_trunk #do not add tags: or the CI will re-fire on the tagging step; adding comment for trigger request workflow_dispatch: inputs: From 5a98fdef123c220648aa1ae84c2c7756ec3d17c1 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 3 Jun 2025 10:06:08 -0600 Subject: [PATCH 067/130] Same typo - brave new world --- .github/workflows/mainbuild.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mainbuild.yml b/.github/workflows/mainbuild.yml index df469e3..da489f3 100644 --- a/.github/workflows/mainbuild.yml +++ b/.github/workflows/mainbuild.yml @@ -42,9 +42,9 @@ jobs: if [[ "${{ github.event.inputs.publish }}" == "true" ]]; then SHOULD_PUBLISH="true" fi - # Or if it's a push to main or dev-trunk and we have a PAT + # Or if it's a push to main or dev_trunk and we have a PAT elif [[ "${{ github.event_name }}" == "push" && -n "$ADO_PAT" ]]; then - if [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.ref }}" == "refs/heads/dev-trunk" ]]; then + if [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.ref }}" == "refs/heads/dev_trunk" ]]; then SHOULD_PUBLISH="true" fi fi @@ -106,7 +106,7 @@ jobs: ADO_PAT: ${{ secrets.ADO_PAT }} - name: Tag repo with new version - if: env.SHOULD_PUBLISH == 'true' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev-trunk') + if: env.SHOULD_PUBLISH == 'true' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev_trunk') shell: pwsh run: | $versionPath = "_tasks/environments.json" From 3e5e804277b3edccd30281c2b9cd897b69b828bd Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 3 Jun 2025 10:35:50 -0600 Subject: [PATCH 068/130] Totally forgot to update environments.json for new guid preparation --- _tasks/environments.json | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/_tasks/environments.json b/_tasks/environments.json index 395784a..0e36373 100644 --- a/_tasks/environments.json +++ b/_tasks/environments.json @@ -26,6 +26,18 @@ "EGGetALCompiler": { "location": "bc-tools-extension/Get-VSIXCompiler/task.json", "taskGuid": "6af71bc7-0491-4779-8e7d-bbc4eeeadbe0" + }, + "EGGetBCCompanies": { + "location": "bc-tools-extension/Get-ListOfCompanies/task.json", + "taskGuid": "0d4e6693-bdcb-47c0-a373-67a34549da07" + }, + "EGGetBCModules": { + "location": "bc-tools-extension/Get-ListOfModules/task.json", + "taskGuid": "4a9b312e-5e0d-4239-a254-3ac8808c3c73" + }, + "EGDeployBCModule": { + "location": "bc-tools-extension/Publish-BCModuleToTenant/task.json", + "taskGuid": "def7c0a0-0d00-4f62-ae3f-7f084561e721" } } }, @@ -50,7 +62,19 @@ "EGGetALCompiler": { "location": "bc-tools-extension/Get-VSIXCompiler/task.json", "taskGuid": "c8d27640-a774-4ed5-a2bd-b2a6d22963e7" - } + }, + "EGGetBCCompanies": { + "location": "bc-tools-extension/Get-ListOfCompanies/task.json", + "taskGuid": "7a875156-f18f-4595-868e-0a1d978b882a" + }, + "EGGetBCModules": { + "location": "bc-tools-extension/Get-ListOfModules/task.json", + "taskGuid": "0dd565c5-c24f-48a8-9414-9e3de695d934" + }, + "EGDeployBCModule": { + "location": "bc-tools-extension/Publish-BCModuleToTenant/task.json", + "taskGuid": "7315a985-6da9-4b4a-bae9-56b04fc492fd" + } } } } \ No newline at end of file From d7c8356e3c13cb2c00219e96fcc4a385585751d0 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 5 Jun 2025 11:15:43 -0600 Subject: [PATCH 069/130] Improve logging and attempt to fix file upload --- bc-tools-extension/_common/CommonTools.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 2dc229e..bf3b6bd 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -1,5 +1,6 @@ const path = require('path'); const fs = require('fs/promises'); +const { stat } = require('fs'); const testMode = process.env.INPUT_TESTMODE; @@ -238,6 +239,9 @@ async function getInstallationStatus(token, tenantId, environmentName, companyId throw new Error('Extension deployment status query failed'); } + console.debug('API response (getInstallationStatus)'); + console.debug(await response.json()); + const data = await response.json(); return data.value; } @@ -383,7 +387,20 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI try { await fs.access(filePathAndName); + + const stats = await fs.stat(filePathAndName); + console.log(`File found: ${filePathAndName} (${stats.size} bytes)`); + const fileBuffer = await fs.readFile(filePathAndName); + console.debug(`Prepared file buffer of ${stats.size} bytes from file`); + + console.debug('Uploading file to: ', apiUrl); + console.debug('Headers:', { + 'Authorization': '[REDACTED]', + 'Content-Type': 'application/octet-stream', + 'If-Match': '*' + }); + const response = await fetch(apiUrl, { method: 'PATCH', headers: { From abfac0382e083af9c24ebffc5a6e8ba663ca2f1b Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 5 Jun 2025 12:56:00 -0600 Subject: [PATCH 070/130] Add latency because I think I'm outrunning the API perhaps? --- .../function_Publish-BCModuleToTenant.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index de68d05..aa6e925 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -41,6 +41,8 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); console.log('********** uploadInstallationFile'); let resulting = await commonTools.uploadInstallationFile(token, tenantId, environmentName, companyId, extId, filePath); console.log(resulting ?? 'resulting succeeded'); + console.log('Waiting 5 seconds to allow backend to process file...'); + await new Promise(resolve => setTimeout(resolve, 5000)); console.log('********** callNavUploadCommand'); let callUpload = await commonTools.callNavUploadCommand(token, tenantId, environmentName, companyId, extId); console.log('********** now awaiting response'); From 1c0b12066cfdd3dd8bb67b280eb0fd360d0fbb2e Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 5 Jun 2025 13:42:06 -0600 Subject: [PATCH 071/130] Troubleshooting the publish routine --- .../function_Publish-BCModuleToTenant.js | 7 +++-- bc-tools-extension/_common/CommonTools.js | 27 ++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index aa6e925..848dc52 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -30,16 +30,19 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); const token = await commonTools.getToken(tenantId, clientId, clientSecret); console.log('********** createInstallationBookmark'); let test = await commonTools.createInstallationBookmark(token, tenantId, environmentName, companyId); - console.log(test); let extId; + let odata_etag; if (Array.isArray(test)) { extId = test[0].systemId; + odata_etag = test[0]['@odata.etag']; } else { extId = test.systemId; + odata_etag = test['@odata.etag']; } console.log('ExtId (the bookmark): ', extId); + console.log('@odata.etag: ', odata_etag); console.log('********** uploadInstallationFile'); - let resulting = await commonTools.uploadInstallationFile(token, tenantId, environmentName, companyId, extId, filePath); + let resulting = await commonTools.uploadInstallationFile(token, tenantId, environmentName, companyId, extId, filePath, odata_etag); console.log(resulting ?? 'resulting succeeded'); console.log('Waiting 5 seconds to allow backend to process file...'); await new Promise(resolve => setTimeout(resolve, 5000)); diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index bf3b6bd..02c89c2 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -381,9 +381,10 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp * @param {string} filePathAndName * @returns {boolean} true if successful; false if not */ -async function uploadInstallationFile(token, tenantId, environmentName, companyId, operationId, filePathAndName) { +async function uploadInstallationFile(token, tenantId, environmentName, companyId, operationId, filePathAndName, odata_etag) { const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/extensionContent`; console.debug('API (uploadInstallationFile): ', apiUrl); + console.debug('@odata.etag: ', odata_etag); try { await fs.access(filePathAndName); @@ -398,7 +399,7 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI console.debug('Headers:', { 'Authorization': '[REDACTED]', 'Content-Type': 'application/octet-stream', - 'If-Match': '*' + 'If-Match': odata_etag }); const response = await fetch(apiUrl, { @@ -406,7 +407,7 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/octet-stream', - 'If-Match': '*' + 'If-Match': odata_etag }, body: fileBuffer }); @@ -437,9 +438,10 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI } } -async function callNavUploadCommand(token, tenantId, environmentName, companyId, operationId) { +async function callNavUploadCommand(token, tenantId, environmentName, companyId, operationId, odata_etag) { const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/Microsoft.NAV.upload`; console.debug('API (callNavUploadCommand): ', apiUrl); + console.debug('@odata.etag: ', odata_etag) try { console.log('Preparing to call Microsoft.NAV.upload'); @@ -448,7 +450,8 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json', - 'Accept-Encoding': 'gzip, deflate, br' // exclude 'br' + 'Accept-Encoding': 'gzip, deflate, br', + 'If-Match': odata_etag } }); @@ -459,8 +462,20 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, console.error(error); throw new Error('Extension upload call query failed'); } - console.debug('Made call to Microsoft.NAV.upload; response code: ', response.status); + + console.debug('Making a quick check to see if the bookmark still exists in the same form....'); + let quickCheck = await createInstallationBookmark(token, tenantId, environmentName, companyId); + + console.debug(quickCheck); + console.debug('Original Id:', operationId); + console.debug('Current Id: ', quickCheck.operationId); + console.debug(''); + console.debug('Original eTag:', odata_etag); + console.debug('Current eTag: ', quickCheck['@odata.etag']); + console.debug(''); + console.debug('IF THESE DO NOT MATCH, IT MEANS THAT THE UPLOAD COMMAND DESTROYED THE ORIGINAL.'); + } catch (err) { console.error('Error during call: ', err.name, err.message); throw err; From 867e610ce192c7f3c2d2b00f982994423ca5be09 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 5 Jun 2025 13:59:27 -0600 Subject: [PATCH 072/130] Still troubleshooting wait response and upload routines --- .../function_Publish-BCModuleToTenant.js | 5 ++++- bc-tools-extension/_common/CommonTools.js | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index 848dc52..4c56ec3 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -47,10 +47,13 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); console.log('Waiting 5 seconds to allow backend to process file...'); await new Promise(resolve => setTimeout(resolve, 5000)); console.log('********** callNavUploadCommand'); - let callUpload = await commonTools.callNavUploadCommand(token, tenantId, environmentName, companyId, extId); + let callUpload = await commonTools.callNavUploadCommand(token, tenantId, environmentName, companyId, extId, odata_etag); console.log('********** now awaiting response'); if (!skipPolling) { let responseCallback = await commonTools.waitForResponse(token, tenantId, environmentName, companyId, extId, pollingFrequency, maxTimeout); + console.debug('*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG'); + console.debug(responseCallback); + console.debug('*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG'); } console.log('********** done'); } diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 02c89c2..f5b79c2 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -487,11 +487,11 @@ async function waitForResponse(token, tenantId, environmentName, companyId, oper const startTimeStamp = Date.now(); let currentTimeStamp = Date.now(); - console.log(`Waiting an initial ${waitTime} seconds before polling...`); + console.log(`Waiting an initial 2 seconds before polling...`); + await sleep(2); let manualBreak = false; do { - await sleep(waitTime); let thisCheck = await getInstallationStatus(token, tenantId, environmentName, companyId, operationId); if (Array.isArray(thisCheck)) { if (thisCheck.length === 0) { @@ -509,6 +509,7 @@ async function waitForResponse(token, tenantId, environmentName, companyId, oper } } console.debug(Date.now(), ': checked progress, result:', thisCheck[0].status); + if (!parseBool(manualBreak)) { await sleep(waitTime) }; } while ((((currentTimeStamp - startTimeStamp) / 1000) < maxWaitTime) && !parseBool(manualBreak)); } From 66a8941ca6cccc7622cd02d76b3bc6e11097d64f Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 5 Jun 2025 14:26:26 -0600 Subject: [PATCH 073/130] Tilt at that windmill! --- bc-tools-extension/_common/CommonTools.js | 24 +++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index f5b79c2..d8276c8 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -445,7 +445,7 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, try { console.log('Preparing to call Microsoft.NAV.upload'); - const response = await fetch(apiUrl, { + let response = await fetch(apiUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, @@ -458,9 +458,25 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, console.log('Call to Microsoft.NAV.upload successful? ¯\\_(ツ)_/¯ It\'s not like Microsoft tells us...'); if (!response.ok) { console.error('Failed to call Microsoft.NAV.upload for deployment: ', response.status); - const error = await response.text(); - console.error(error); - throw new Error('Extension upload call query failed'); + if (response.status === 409) { + let refreshCheck = await createInstallationBookmark(token, tenantId, environmentName, companyId); + console.log('Original odata.etag: ', odata_etag); + odata_etag = refreshCheck['@odata.etag']; + console.log('Refreshed odata.etag:', odata_etag); + response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json', + 'Accept-Encoding': 'gzip, deflate, br', + 'If-Match': odata_etag + } + }); + } else { + const error = await response.text(); + console.error(error); + throw new Error('Extension upload call query failed'); + } } console.debug('Made call to Microsoft.NAV.upload; response code: ', response.status); From e93fe40d2a7b95b56516d8ae355ff7512e6b0531 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 5 Jun 2025 15:08:39 -0600 Subject: [PATCH 074/130] Final candidate version for debug release --- _tasks/environments.json | 2 +- .../function_Publish-BCModuleToTenant.js | 2 +- bc-tools-extension/RELEASE.md | 7 ++++++- bc-tools-extension/_common/CommonTools.js | 14 +------------- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/_tasks/environments.json b/_tasks/environments.json index 0e36373..1f82c30 100644 --- a/_tasks/environments.json +++ b/_tasks/environments.json @@ -2,7 +2,7 @@ "version": { "major": 0, "minor": 1, - "patch": 5, + "patch": 6, "build": 0 }, "dev": { diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index 4c56ec3..9c88bff 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -41,9 +41,9 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); } console.log('ExtId (the bookmark): ', extId); console.log('@odata.etag: ', odata_etag); + console.log(''); console.log('********** uploadInstallationFile'); let resulting = await commonTools.uploadInstallationFile(token, tenantId, environmentName, companyId, extId, filePath, odata_etag); - console.log(resulting ?? 'resulting succeeded'); console.log('Waiting 5 seconds to allow backend to process file...'); await new Promise(resolve => setTimeout(resolve, 5000)); console.log('********** callNavUploadCommand'); diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index 349bd38..84138b5 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -1,5 +1,6 @@ # Release Notes - BCBuildTasks Extension +- [Version: 0.1.6](#version-016) - [Version: 0.1.5](#version-015) + [Feature Release](#feature-release) * [New Features](#new-features) @@ -23,7 +24,11 @@ * [Example Pipeline Usage](#example-pipeline-usage) * [Known Limitations](#known-limitations) * [Support](#support) - + +# Version: 0.1.6 + +- Addresses bug [#26](https://github.com/crazycga/bcdevopsextension/issues/26): Publish command not working correctly + # Version: 0.1.5 **Release Date:** 2025-06-03 diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index d8276c8..6ae57a2 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -459,6 +459,7 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, if (!response.ok) { console.error('Failed to call Microsoft.NAV.upload for deployment: ', response.status); if (response.status === 409) { + console.log('Wait a second, that was a 409; this means that the @odata.etag is stale, let me try to get another one'); let refreshCheck = await createInstallationBookmark(token, tenantId, environmentName, companyId); console.log('Original odata.etag: ', odata_etag); odata_etag = refreshCheck['@odata.etag']; @@ -479,19 +480,6 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, } } console.debug('Made call to Microsoft.NAV.upload; response code: ', response.status); - - console.debug('Making a quick check to see if the bookmark still exists in the same form....'); - let quickCheck = await createInstallationBookmark(token, tenantId, environmentName, companyId); - - console.debug(quickCheck); - console.debug('Original Id:', operationId); - console.debug('Current Id: ', quickCheck.operationId); - console.debug(''); - console.debug('Original eTag:', odata_etag); - console.debug('Current eTag: ', quickCheck['@odata.etag']); - console.debug(''); - console.debug('IF THESE DO NOT MATCH, IT MEANS THAT THE UPLOAD COMMAND DESTROYED THE ORIGINAL.'); - } catch (err) { console.error('Error during call: ', err.name, err.message); throw err; From ef1d5561183b2a567ee9ff624c677fc8bfa1e9d2 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 5 Jun 2025 15:58:30 -0600 Subject: [PATCH 075/130] Error capture the new odata.etag properly --- bc-tools-extension/_common/CommonTools.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 6ae57a2..8f93825 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -462,7 +462,11 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, console.log('Wait a second, that was a 409; this means that the @odata.etag is stale, let me try to get another one'); let refreshCheck = await createInstallationBookmark(token, tenantId, environmentName, companyId); console.log('Original odata.etag: ', odata_etag); - odata_etag = refreshCheck['@odata.etag']; + if (Array.isArray(refreshCheck)) { + odata_etag = refreshCheck[0]['@odata.etag']; + } else { + odata_etag = refreshCheck['@odata.etag']; + } console.log('Refreshed odata.etag:', odata_etag); response = await fetch(apiUrl, { method: 'POST', From 5bb15ca6b245de43274a74d1845fa7b135bf970b Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 5 Jun 2025 16:27:11 -0600 Subject: [PATCH 076/130] Strangely the upload routine now balks at the if-match on the call to nav.upload; removing --- bc-tools-extension/_common/CommonTools.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 8f93825..ae09fd4 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -450,8 +450,7 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json', - 'Accept-Encoding': 'gzip, deflate, br', - 'If-Match': odata_etag + 'Accept-Encoding': 'gzip, deflate, br' } }); @@ -473,8 +472,7 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json', - 'Accept-Encoding': 'gzip, deflate, br', - 'If-Match': odata_etag + 'Accept-Encoding': 'gzip, deflate, br' } }); } else { From 03e18cd813dd15506dc6dbd4de74bb39953eca49 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 12:18:56 -0600 Subject: [PATCH 077/130] Minor corrections to timing and fixes to publish routine Refers to #26 --- bc-tools-extension/_common/CommonTools.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index ae09fd4..ff583c8 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -240,9 +240,9 @@ async function getInstallationStatus(token, tenantId, environmentName, companyId } console.debug('API response (getInstallationStatus)'); - console.debug(await response.json()); - const data = await response.json(); + + console.debug(data); return data.value; } @@ -491,7 +491,7 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, async function waitForResponse(token, tenantId, environmentName, companyId, operationId, waitTime, maxWaitTime) { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms * 1000)); const startTimeStamp = Date.now(); - let currentTimeStamp = Date.now(); + console.log(`Waiting an initial 2 seconds before polling...`); await sleep(2); @@ -516,6 +516,7 @@ async function waitForResponse(token, tenantId, environmentName, companyId, oper } console.debug(Date.now(), ': checked progress, result:', thisCheck[0].status); if (!parseBool(manualBreak)) { await sleep(waitTime) }; + let currentTimeStamp = Date.now(); } while ((((currentTimeStamp - startTimeStamp) / 1000) < maxWaitTime) && !parseBool(manualBreak)); } From bc9962f5680bc1d9d227ca96a04d62fceddfb8c2 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 12:35:38 -0600 Subject: [PATCH 078/130] Update documentation, etc. for new version Fixes #26 --- README.md | 13 +++++++++++++ _tasks/environments.json | 2 +- bc-tools-extension/README.md | 16 ++++++++++++++++ bc-tools-extension/RELEASE.md | 13 +++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a12abc9..a3d90d4 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,16 @@ The problem indicated in the `overview.md` has been superseded by events that ca "path": "README.md" }, ``` + +## Developer Notes + +| Failure | Likely Cause | +| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **StandardOut has not been redirected or the process hasn't started yet.** | This occurs when the PowerShell task in the pipeline is malformed — most often caused by **missing script content**, a **missing filePath**, or **bad path slashes** (especially `\` instead of `/` on Linux agents). It can also happen if the script crashes before the pipeline task properly starts.| +| **callNavUploadCommand appears to succeed, but nothing happens afterward** | The call to `Microsoft.NAV.upload` returns HTTP 200 even when it **fails silently**. You must follow up with polling and inspect the deployment status via `extensionDeploymentStatus` to detect actual success or failure.| +| **Polling loop exits after one check, even when status is 'InProgress'**| Caused by calling `response.json()` **twice** in `getInstallationStatus()`, which consumes the response body stream on the first call, leaving the second call empty. This results in undefined values or an early break.| +| **Version number updated, but deployment still fails** | Business Central may not recognize a new version string unless **major/minor/patch are also changed**. Changing only the build metadata (e.g., `1.0.0.123` → `1.0.0.124`) may be ignored depending on caching or database lag. Try bumping `1.0.1` instead.| +| **Multiple extensions in deployment status, loop picks wrong one**| The polling loop may pick the **wrong `operationId`** from `extensionDeploymentStatus` if you're not filtering by your own ID or name. Always prefer to filter by `operationId` if known. Otherwise, match by `name`, `publisher`, and `appVersion` to ensure accuracy. | +| **Pipeline script can't find `app.json`** | Caused by relying on `$(Build.SourcesDirectory)` without checking the actual folder layout. Paths can change depending on repo structure, multi-stage pipelines, or if `checkout: none` was accidentally set. Prefer dynamic resolution using recursive search and fail early if multiple matches are found. | +| **Upload call fails with 409 (Conflict)** | This means your `@odata.etag` is stale. Re-fetch the latest extension upload record before retrying the `upload` call. The `etag` is not durable across upload+retry attempts. | +| **'undici' not found** | Some agents may not have `undici` preinstalled. The script attempts auto-install, but this can fail due to **network restrictions**, **read-only agents**, or **restricted `npm` execution**. Make sure your agent allows install or prebundle `node_modules` if needed.| diff --git a/_tasks/environments.json b/_tasks/environments.json index 1f82c30..7c4081f 100644 --- a/_tasks/environments.json +++ b/_tasks/environments.json @@ -2,7 +2,7 @@ "version": { "major": 0, "minor": 1, - "patch": 6, + "patch": 7, "build": 0 }, "dev": { diff --git a/bc-tools-extension/README.md b/bc-tools-extension/README.md index d5349ba..621b5a0 100644 --- a/bc-tools-extension/README.md +++ b/bc-tools-extension/README.md @@ -297,6 +297,22 @@ There is not much more control that is provided and even the response codes from ``` +## Common Failures + +Here are some common failures and their likely causes: + +|Failure|Cause| +|---|---| +|**Immediate fail on deploy** | An immediate failure often means the extension’s **version number hasn’t changed**. Business Central may retain internal metadata **even if** the extension was unpublished and removed. This can cause silent rejections during re-deploy. To confirm, try a manual upload — the web interface will usually surface an error message that the API silently swallows. | +| **Extension never installs / stuck in “InProgress”** | Business Central backend is overloaded or stalled (e.g., schema sync issues, queued deployments). | Increase `PollingTimeout` and check the BC admin center for other queued extensions or backend delays. | +| **Extension fails with no visible error message** | The BC API may suppress detailed errors. Often due to invalid dependencies, permission errors, or duplicate version conflicts. | Try uploading the extension manually through the BC UI to surface hidden error messages. | +| **Authentication fails** | Incorrect or expired `ClientSecret`; or app registration missing permissions. | Ensure app has Application permissions, `API.ReadWrite.All`, and admin consent granted. Rotate secret if expired. | +| **Upload succeeds (204) but deployment never completes** | 204 means "upload accepted", not "installed". Backend may not trigger processing. | Let polling run. If it never transitions, retry `Microsoft.NAV.upload` call or re-check bookmark. | +| **Multiple extension versions appear in BC** | Business Central retains ghost entries if version number isn't changed. | Always increment the version number (`1.0.0.x`) for every build. | +| **Pipeline fails to find `app.json`** | Folder structure doesn't match expected default; `PathToAppJson` may be incorrect. | Confirm folder layout. Use an inline `ls` or echo step to validate paths before running. | +| **Polling hangs until timeout** | Deployment didn’t register; call to `Microsoft.NAV.upload` may have silently failed. | Recheck that upload succeeded and bookmark is valid. Consider increasing logging verbosity. | +| **ENOENT / ETAG errors during upload** | Missing `.app` file or stale `@odata.etag` from a previous upload attempt. | Confirm `Build Package` step ran and `.app` file exists. If needed, reacquire a fresh bookmark with valid ETAG. | + ## Security & Trust These tasks are designed for internal use and are provided under the Evergrowth publisher namespace. All task logic is exposed via wrapper scripts for transparency and audit. Source code is available on GitHub. diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index 84138b5..34a364f 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -1,5 +1,6 @@ # Release Notes - BCBuildTasks Extension +- [Version: 0.1.7](#version-017) - [Version: 0.1.6](#version-016) - [Version: 0.1.5](#version-015) + [Feature Release](#feature-release) @@ -25,6 +26,18 @@ * [Known Limitations](#known-limitations) * [Support](#support) +# Version: 0.1.7 + +## Fixes + +- Fixed [#26](https://github.com/crazycga/bcdevopsextension/issues/26): Publish command not working correctly; should be fixed now. +- Timeout command wasn't working properly, that has been fixed as well. + +## Improvements + +- Added developer notes to the repo README.md. +- Added possible errors and causal factors to README.md and VS Marketplace page. + # Version: 0.1.6 - Addresses bug [#26](https://github.com/crazycga/bcdevopsextension/issues/26): Publish command not working correctly From f81e5357d993d2d90135bf1d7145d65501703e64 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 14:42:24 -0600 Subject: [PATCH 079/130] Modify pipeline build to only tag release versions --- .github/workflows/mainbuild.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mainbuild.yml b/.github/workflows/mainbuild.yml index da489f3..8a1a763 100644 --- a/.github/workflows/mainbuild.yml +++ b/.github/workflows/mainbuild.yml @@ -106,7 +106,7 @@ jobs: ADO_PAT: ${{ secrets.ADO_PAT }} - name: Tag repo with new version - if: env.SHOULD_PUBLISH == 'true' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev_trunk') + if: env.SHOULD_PUBLISH == 'true' && (github.ref == 'refs/heads/main') shell: pwsh run: | $versionPath = "_tasks/environments.json" From 1bf566499a0def5a434dfaf060849ddfb48310eb Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 15:14:39 -0600 Subject: [PATCH 080/130] Fix timing declaration issue --- bc-tools-extension/_common/CommonTools.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index ff583c8..4eeaead 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -491,7 +491,7 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, async function waitForResponse(token, tenantId, environmentName, companyId, operationId, waitTime, maxWaitTime) { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms * 1000)); const startTimeStamp = Date.now(); - + let currentTimeStamp = Date.now(); console.log(`Waiting an initial 2 seconds before polling...`); await sleep(2); @@ -516,7 +516,7 @@ async function waitForResponse(token, tenantId, environmentName, companyId, oper } console.debug(Date.now(), ': checked progress, result:', thisCheck[0].status); if (!parseBool(manualBreak)) { await sleep(waitTime) }; - let currentTimeStamp = Date.now(); + currentTimeStamp = Date.now(); } while ((((currentTimeStamp - startTimeStamp) / 1000) < maxWaitTime) && !parseBool(manualBreak)); } From 03c5799c23ca24a6c734428b11c2544f2af2be16 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 15:31:32 -0600 Subject: [PATCH 081/130] Fix up some logging, make things a little prettier --- .../function_Publish-BCModuleToTenant.js | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index 9c88bff..980f3b9 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -17,7 +17,7 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); console.log(`TenantId: ${tenantId}`); console.log(`EnvironmentName: ${environmentName}`); console.log(`ClientId: ${clientId}`); - console.log(`ClientSecret: 'Yeah, right, try again'`); + console.log(`ClientSecret: [REDACTED]`); console.log(`CompanyId: ${companyId}`); console.log(`AppFilePath: ${filePath}`); console.log(`SkipPolling: ${skipPolling}`); @@ -26,9 +26,9 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); console.log(''); try { - console.log('********** getToken'); + console.log('>>>>>>>>>> getToken'); const token = await commonTools.getToken(tenantId, clientId, clientSecret); - console.log('********** createInstallationBookmark'); + console.log('>>>>>>>>>> createInstallationBookmark'); let test = await commonTools.createInstallationBookmark(token, tenantId, environmentName, companyId); let extId; let odata_etag; @@ -39,23 +39,20 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); extId = test.systemId; odata_etag = test['@odata.etag']; } - console.log('ExtId (the bookmark): ', extId); - console.log('@odata.etag: ', odata_etag); - console.log(''); - console.log('********** uploadInstallationFile'); + console.debug('>>>>>>>>>> ExtId (the bookmark): ', extId); + console.debug('>>>>>>>>>> @odata.etag: ', odata_etag); + console.debug(''); + console.log('>>>>>>>>>> uploadInstallationFile'); let resulting = await commonTools.uploadInstallationFile(token, tenantId, environmentName, companyId, extId, filePath, odata_etag); console.log('Waiting 5 seconds to allow backend to process file...'); await new Promise(resolve => setTimeout(resolve, 5000)); - console.log('********** callNavUploadCommand'); + console.log('>>>>>>>>>> callNavUploadCommand'); let callUpload = await commonTools.callNavUploadCommand(token, tenantId, environmentName, companyId, extId, odata_etag); - console.log('********** now awaiting response'); + console.log('>>>>>>>>>> now awaiting response'); if (!skipPolling) { let responseCallback = await commonTools.waitForResponse(token, tenantId, environmentName, companyId, extId, pollingFrequency, maxTimeout); - console.debug('*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG'); - console.debug(responseCallback); - console.debug('*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG*DEBUG'); } - console.log('********** done'); + console.log('>>>>>>>>>> done'); } catch (error) { console.error('Error: ', error.message); From 709c941e642cb741505be6ba370a07bc7913e4c2 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 15:55:59 -0600 Subject: [PATCH 082/130] Prettification of some logs --- .../Get-ListOfCompanies/function_Get-ListOfCompanies.js | 8 ++++---- .../Get-ListOfModules/function_Get-ListOfModules.js | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 1b2c9c8..5b10cd2 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -57,12 +57,12 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD console.log('**********************************************************'); const expectedPath = path.resolve(__dirname, '../_common/CommonTools.js'); - console.log('👀 Trying to stat:', expectedPath); + console.log('Trying to stat:', expectedPath); try { fs.statSync(expectedPath); - console.log('✅ Found commonTools at expected path'); + console.log('Found commonTools at expected path'); } catch { - console.log('❌ commonTools NOT found at expected path'); + console.log('commonTools NOT found at expected path'); } } @@ -76,7 +76,7 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD companies.forEach((company, idx) => { const name = company.name; const id = company.id; - console.log(`${idx + 1}. ${company.name} (ID: ${company.id})`); + console.log(`${(idx + 1).toString().padStart(3)}. (ID: ${company.id}) ${company.name.padEnd(80)}`); }); } catch (error) { diff --git a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js index a276017..555ee20 100644 --- a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js +++ b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js @@ -17,9 +17,11 @@ const excludeMicrosoft = process.env.INPUT_EXCLUDEMICROSOFT; console.log('Modules:'); modules.forEach((module, idx) => { - const name = module.name; + const name = module.displayName; const id = module.id; - console.log(`${idx + 1}. ${module.displayName} (ID: ${module.id})`); + const pid = module.packageId + const version = `${module.versionMajor}.${module.versionMinor}.${module.versionBuild}.${module.versionRevision}`; + console.log(`${(idx + 1).toString().padStart(3)}. ${name.padEnd(60)} (Module ID: ${id}) v${version.padEnd(20)} (Package ID: ${pid})`); }); } catch (error) { console.error('Error: ', error.message); From 7f83364d55b43b94f4df7889c4903a6d421909da Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 15:57:55 -0600 Subject: [PATCH 083/130] Added changes to RELEASE.md --- bc-tools-extension/RELEASE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index 34a364f..b0faf24 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -6,8 +6,8 @@ + [Feature Release](#feature-release) * [New Features](#new-features) + [1. **EGGetBCCompanies**](#1-eggetbccompanies) - + [2. EGGetBCModules](#2-eggetbcmodules) - + [3. EGDeployBCModule](#3-egdeploybcmodule) + + [2. **EGGetBCModules**](#2-eggetbcmodules) + + [3. **EGDeployBCModule**](#3-egdeploybcmodule) - [Version: 0.1.4](#version-014) + [Improvement Release](#improvement-release) * [New Features](#new-features-1) @@ -37,6 +37,8 @@ - Added developer notes to the repo README.md. - Added possible errors and causal factors to README.md and VS Marketplace page. +- Prettified some of the output logs for `EGGetBCCompanies` and `EGGetBCModules`. +- Cleaned up some logging in `EGDeployBCModule`. # Version: 0.1.6 From 34ccec306500b71eab792a5418a1d1c37e4dc194 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 16:55:40 -0600 Subject: [PATCH 084/130] Clean up logging in getInstallationStatus; migrate to logger instead of straight console --- .../function_Get-ListOfCompanies.js | 48 ++++--- .../function_Get-ListOfModules.js | 7 +- .../function_Publish-BCModuleToTenant.js | 45 +++---- bc-tools-extension/_common/CommonTools.js | 124 ++++++++++-------- 4 files changed, 123 insertions(+), 101 deletions(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 5b10cd2..33bd2a7 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; @@ -15,54 +16,61 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD // tool that I discovered that _each task_ required a _copy_ of the "_common" directory if (extremeDebugMode) { - console.log('process.cwd():', process.cwd()); // where the process was started - console.log('__dirname:', __dirname); // where the current script file resides + console.log('Testing logger function:'); + logger.debug('This is a logger.debug command'.padStart(10)); + logger.info('This is a logger.info command'.padStart(10)); + logger.warn('This is a logger.warn command'.padStart(10)); + logger.error('This is a logger.error command'.padStart(10)); + logger.info(''); + logger.info(''); + logger.debug('process.cwd():', process.cwd()); // where the process was started + logger.debug('__dirname:', __dirname); // where the current script file resides - console.log('Enumerating parent:'); + logger.debug('Enumerating parent:'); fs.readdir('..', (err, files) => { if (err) { - console.error('Error reading directory: ', err); + logger.error('Error reading directory: ', err); return; } - console.log('Files in parent directory:'); + logger.debug('Files in parent directory:'); files.forEach(file => { - console.log(file); + logger.debug(file); }); }); - console.log('Enumerating current:'); + logger.debug('Enumerating current:'); fs.readdir('.', (err, files) => { if (err) { - console.error('Error reading directory: ', err); + logger.error('Error reading directory: ', err); return; } - console.log('Files in parent directory:'); + logger.debug('Files in parent directory:'); files.forEach(file => { - console.log(file); + logger.debug(file); }); }); let current = __dirname; let depth = 5; for (let i = 0; i < depth; i++) { - console.log(`\n📂 Contents of: ${current}`); + logger.debug(`\nContents of: ${current}`); try { const files = fs.readdirSync(current); files.forEach(f => console.log(' -', f)); } catch (e) { - console.error(` (Error reading ${current}: ${e.message})`); + logger.error(` (Error reading ${current}: ${e.message})`); } current = path.resolve(current, '..'); } - console.log('**********************************************************'); + logger.debug('**********************************************************'); const expectedPath = path.resolve(__dirname, '../_common/CommonTools.js'); - console.log('Trying to stat:', expectedPath); + logger.debug('Trying to stat:', expectedPath); try { fs.statSync(expectedPath); - console.log('Found commonTools at expected path'); + logger.debug('Found commonTools at expected path'); } catch { - console.log('commonTools NOT found at expected path'); + logger.debug('commonTools NOT found at expected path'); } } @@ -72,14 +80,12 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD const token = await commonTools.getToken(tenantId, clientId, clientSecret); const companies = await commonTools.getCompanies(token, tenantId, environmentName); - console.log('Companies:'); + logger.info('Companies:'); companies.forEach((company, idx) => { - const name = company.name; - const id = company.id; - console.log(`${(idx + 1).toString().padStart(3)}. (ID: ${company.id}) ${company.name.padEnd(80)}`); + logger.info(`${(idx + 1).toString().padStart(3)}. (ID: ${company.id}) ${company.name.padEnd(80)}`); }); } catch (error) { - console.error('Error: ', error.message); + logger.error('Error: ', error.message); } })(); \ No newline at end of file diff --git a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js index 555ee20..c65fa5d 100644 --- a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js +++ b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js @@ -1,5 +1,6 @@ const path = require('path'); const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; @@ -15,15 +16,15 @@ const excludeMicrosoft = process.env.INPUT_EXCLUDEMICROSOFT; const token = await commonTools.getToken(tenantId, clientId, clientSecret); const modules = await commonTools.getModules(token, tenantId, environmentName, companyId, moduleId, excludeMicrosoft); - console.log('Modules:'); + logger.info('Modules:'); modules.forEach((module, idx) => { const name = module.displayName; const id = module.id; const pid = module.packageId const version = `${module.versionMajor}.${module.versionMinor}.${module.versionBuild}.${module.versionRevision}`; - console.log(`${(idx + 1).toString().padStart(3)}. ${name.padEnd(60)} (Module ID: ${id}) v${version.padEnd(20)} (Package ID: ${pid})`); + logger.info(`${(idx + 1).toString().padStart(3)}. ${name.padEnd(60)} (Module ID: ${id}) v${version.padEnd(20)} (Package ID: ${pid})`); }); } catch (error) { - console.error('Error: ', error.message); + logger.error('Error: ', error.message); } })(); \ No newline at end of file diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index 980f3b9..117a3aa 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -1,5 +1,6 @@ const path = require('path'); const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; @@ -13,22 +14,22 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); (async () => { - console.log("Calling deployment of module with the following parameters:"); - console.log(`TenantId: ${tenantId}`); - console.log(`EnvironmentName: ${environmentName}`); - console.log(`ClientId: ${clientId}`); - console.log(`ClientSecret: [REDACTED]`); - console.log(`CompanyId: ${companyId}`); - console.log(`AppFilePath: ${filePath}`); - console.log(`SkipPolling: ${skipPolling}`); - console.log(`PollingFrequency: ${pollingFrequency}`); - console.log(`MaxPollingTimeout: ${maxTimeout}`); - console.log(''); + logger.info("Calling deployment of module with the following parameters:"); + logger.info(`TenantId: ${tenantId}`); + logger.info(`EnvironmentName: ${environmentName}`); + logger.info(`ClientId: ${clientId}`); + logger.info(`ClientSecret: [REDACTED]`); + logger.info(`CompanyId: ${companyId}`); + logger.info(`AppFilePath: ${filePath}`); + logger.info(`SkipPolling: ${skipPolling}`); + logger.info(`PollingFrequency: ${pollingFrequency}`); + logger.info(`MaxPollingTimeout: ${maxTimeout}`); + logger.info(''); try { - console.log('>>>>>>>>>> getToken'); + logger.info('>>>>>>>>>> getToken'); const token = await commonTools.getToken(tenantId, clientId, clientSecret); - console.log('>>>>>>>>>> createInstallationBookmark'); + logger.info('>>>>>>>>>> createInstallationBookmark'); let test = await commonTools.createInstallationBookmark(token, tenantId, environmentName, companyId); let extId; let odata_etag; @@ -39,22 +40,22 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); extId = test.systemId; odata_etag = test['@odata.etag']; } - console.debug('>>>>>>>>>> ExtId (the bookmark): ', extId); - console.debug('>>>>>>>>>> @odata.etag: ', odata_etag); - console.debug(''); - console.log('>>>>>>>>>> uploadInstallationFile'); + logger.debug('>>>>>>>>>> ExtId (the bookmark): ', extId); + logger.debug('>>>>>>>>>> @odata.etag: ', odata_etag); + logger.debug(''); + logger.info('>>>>>>>>>> uploadInstallationFile'); let resulting = await commonTools.uploadInstallationFile(token, tenantId, environmentName, companyId, extId, filePath, odata_etag); - console.log('Waiting 5 seconds to allow backend to process file...'); + logger.info('Waiting 5 seconds to allow backend to process file...'); await new Promise(resolve => setTimeout(resolve, 5000)); - console.log('>>>>>>>>>> callNavUploadCommand'); + logger.info('>>>>>>>>>> callNavUploadCommand'); let callUpload = await commonTools.callNavUploadCommand(token, tenantId, environmentName, companyId, extId, odata_etag); - console.log('>>>>>>>>>> now awaiting response'); + logger.info('>>>>>>>>>> now awaiting response'); if (!skipPolling) { let responseCallback = await commonTools.waitForResponse(token, tenantId, environmentName, companyId, extId, pollingFrequency, maxTimeout); } - console.log('>>>>>>>>>> done'); + logger.info('>>>>>>>>>> done'); } catch (error) { - console.error('Error: ', error.message); + logger.error('Error: ', error.message); } })(); \ No newline at end of file diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 4eeaead..ce77dfb 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -4,9 +4,24 @@ const { stat } = require('fs'); const testMode = process.env.INPUT_TESTMODE; +const logger = { + debug: async function (message) { + console.log(`##vso[task.debug]${message}`); + }, + info: async function (message) { + console.log(message); + }, + warn: async function (message) { + console.log(`##vso[task.warning]${message}`); + }, + error: async function (message) { + console.log(`##vso[task.error]${message}`); + } +}; + // using an environmental variable of TESTMODE = "true" will try and prevent specific JSON posts; it is a left over from initial testing if (parseBool(testMode)) { - console.log(`Invocation received with TestMode: ${testMode}`); + logger.info(`Invocation received with TestMode: ${testMode}`); } // this module uses undici for fetching specifically because the call to Microsoft.NAV.upload will return malformed and node-fetch can't parse it @@ -14,7 +29,7 @@ let fetch; try { fetch = require('undici').fetch; } catch (_) { - console.warn("'undici' not found. Attempting to install..."); + logger.warn("'undici' not found. Attempting to install..."); const projectRoot = path.resolve(__dirname, '..'); const { execSync } = require('child_process'); @@ -100,9 +115,9 @@ async function getToken(tenantId, clientId, clientSecret) { }); if (!response.ok) { - console.error('Failed to acquire token: ', response.status); + logger.error('Failed to acquire token: ', response.status); const error = await response.text(); - console.error(error); + logger.error(error); throw new Error('Authentication failed'); } @@ -129,9 +144,9 @@ async function getCompanies(token, tenantId, environmentName) { }); if (!response.ok) { - console.error('Failed to get companies: ', response.status); + logger.error('Failed to get companies: ', response.status); const error = await response.text(); - console.error(error); + logger.error(error); throw new Error('Company list query failed'); } @@ -166,7 +181,7 @@ async function getModules(token, tenantId, environmentName, companyId, moduleId, apiUrl += `?$filter=${filters.join(" and ")}`; } - console.debug(`API: ${apiUrl}`); + logger.debug(`API: ${apiUrl}`); const response = await fetch(apiUrl, { method: 'GET', @@ -177,9 +192,9 @@ async function getModules(token, tenantId, environmentName, companyId, moduleId, }); if (!response.ok) { - console.error('Failed to get modules: ', response.status); + logger.error('Failed to get modules: ', response.status); const error = await response.text(); - console.error(error); + logger.error(error); throw new Error('Module list query failed'); } @@ -196,9 +211,7 @@ async function confirmModule(token, tenantId, environmentName, companyId, module let checkValue = await getModules(token, tenantId, environmentName, companyId, moduleId); checkValue.forEach((module, idx) => { - const name = module.name; - const id = module.id; - console.debug(`**** ${idx + 1}. ${module.displayName} (ID: ${module.id})`); + logger.debug(`**** ${idx + 1}. ${module.displayName} (ID: ${module.id})`); }); return checkValue.some(m => m.id === moduleId); @@ -222,7 +235,7 @@ async function confirmModule(token, tenantId, environmentName, companyId, module */ async function getInstallationStatus(token, tenantId, environmentName, companyId, operationId) { let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionDeploymentStatus`; - console.debug('API (getInstallationStatus)', apiUrl); + logger.debug('API (getInstallationStatus)', apiUrl); const response = await fetch(apiUrl, { method: 'GET', @@ -233,16 +246,16 @@ async function getInstallationStatus(token, tenantId, environmentName, companyId }); if (!response.ok) { - console.error('Failed to get extension deployments: ', response.status); + logger.error('Failed to get extension deployments: ', response.status); const error = await response.text(); - console.error(error); + logger.error(error); throw new Error('Extension deployment status query failed'); } - console.debug('API response (getInstallationStatus)'); + //console.debug('API response (getInstallationStatus)'); const data = await response.json(); - console.debug(data); + //console.debug(data); return data.value; } @@ -287,7 +300,7 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp // This means, when returning the POST, the return is object.systemId, when returning the GET, the return is object[0].systemId let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload`; - console.debug('API (createInstallationBookmark)', apiUrl); + logger.debug('API (createInstallationBookmark)', apiUrl); // per: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/resources/dynamics_extensionupload if (!schedule || schedule.trim() === "") { @@ -313,8 +326,8 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp }; const _debugBody = await JSON.stringify(body); - console.debug('Request body:'); - console.debug(_debugBody); + logger.debug('Request body:'); + logger.debug(_debugBody); const response = await fetch(apiUrl, { method: 'POST', @@ -334,14 +347,14 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp errorResponse = await response.json(); } catch (e) { const raw = await response.text(); - console.error('Non-JSON error response: ', raw); + logger.error('Non-JSON error response: ', raw); throw new Error('Extension slot creation failed with unknown format'); } - console.error('BC API error response: ', JSON.stringify(errorResponse, null, 2)); + logger.error('BC API error response: ', JSON.stringify(errorResponse, null, 2)); if (status === 400 && errorResponse?.error?.code === "Internal_EntityWithSameKeyExists") { - console.warn('Extension upload already exists - retrieving existing record...'); + logger.warn('Extension upload already exists - retrieving existing record...'); const secondResponse = await fetch(apiUrl, { method: 'GET', headers: { @@ -352,7 +365,7 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp }); if (secondResponse.ok) { - console.log('Found existing record; parsing and returning to routine'); + logger.info('Found existing record; parsing and returning to routine'); const secondValue = await secondResponse.json(); if (Array.isArray(secondValue)) { return secondValue.value[0]; // see internal note above @@ -366,8 +379,8 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp } const data = await response.json(); - console.debug('(createInstallationBookmark) returning: '); - console.debug(data); + logger.debug('(createInstallationBookmark) returning: '); + logger.debug(data); return data; } @@ -383,20 +396,20 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp */ async function uploadInstallationFile(token, tenantId, environmentName, companyId, operationId, filePathAndName, odata_etag) { const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/extensionContent`; - console.debug('API (uploadInstallationFile): ', apiUrl); - console.debug('@odata.etag: ', odata_etag); + logger.debug('API (uploadInstallationFile): ', apiUrl); + logger.debug('@odata.etag: ', odata_etag); try { await fs.access(filePathAndName); const stats = await fs.stat(filePathAndName); - console.log(`File found: ${filePathAndName} (${stats.size} bytes)`); + logger.info(`File found: ${filePathAndName} (${stats.size} bytes)`); const fileBuffer = await fs.readFile(filePathAndName); - console.debug(`Prepared file buffer of ${stats.size} bytes from file`); + logger.info(`Prepared file buffer of ${stats.size} bytes from file`); - console.debug('Uploading file to: ', apiUrl); - console.debug('Headers:', { + logger.debug('Uploading file to: ', apiUrl); + logger.debug('Headers:', { 'Authorization': '[REDACTED]', 'Content-Type': 'application/octet-stream', 'If-Match': odata_etag @@ -415,12 +428,12 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI if (!response.ok) { const error = await response.text(); const errorCode = response.status; - console.error('Upload failed: status code: ', errorCode); - console.error(`Upload failed [${response.status}]: ${error}`); + logger.error('Upload failed: status code: ', errorCode); + logger.error(`Upload failed [${response.status}]: ${error}`); throw new Error('File upload failed.'); } - console.log('Upload successful of:', filePathAndName, 'with a status code of:', response.status); + logger.info('Upload successful of:', filePathAndName, 'with a status code of:', response.status); if (response.status === 204) { return true; @@ -430,9 +443,9 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI } catch (err) { if (err.code === 'ENOENT') { - console.warn('File not found: ', filePathAndName); + logger.warn('File not found: ', filePathAndName); } else { - console.error('Unexpected error during upload: ', err); + logger.error('Unexpected error during upload: ', err); } throw err; } @@ -440,11 +453,11 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI async function callNavUploadCommand(token, tenantId, environmentName, companyId, operationId, odata_etag) { const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/Microsoft.NAV.upload`; - console.debug('API (callNavUploadCommand): ', apiUrl); - console.debug('@odata.etag: ', odata_etag) + logger.debug('API (callNavUploadCommand): ', apiUrl); + logger.debug('@odata.etag: ', odata_etag) try { - console.log('Preparing to call Microsoft.NAV.upload'); + logger.info('Preparing to call Microsoft.NAV.upload'); let response = await fetch(apiUrl, { method: 'POST', headers: { @@ -454,19 +467,19 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, } }); - console.log('Call to Microsoft.NAV.upload successful? ¯\\_(ツ)_/¯ It\'s not like Microsoft tells us...'); + logger.info('Call to Microsoft.NAV.upload successful? ¯\\_(ツ)_/¯ It\'s not like Microsoft tells us...'); if (!response.ok) { - console.error('Failed to call Microsoft.NAV.upload for deployment: ', response.status); + logger.error('Failed to call Microsoft.NAV.upload for deployment: ', response.status); if (response.status === 409) { - console.log('Wait a second, that was a 409; this means that the @odata.etag is stale, let me try to get another one'); + logger.info('Wait a second, that was a 409; this means that the @odata.etag is stale, let me try to get another one'); let refreshCheck = await createInstallationBookmark(token, tenantId, environmentName, companyId); - console.log('Original odata.etag: ', odata_etag); + logger.info('Original odata.etag: ', odata_etag); if (Array.isArray(refreshCheck)) { odata_etag = refreshCheck[0]['@odata.etag']; } else { odata_etag = refreshCheck['@odata.etag']; } - console.log('Refreshed odata.etag:', odata_etag); + logger.info('Refreshed odata.etag:', odata_etag); response = await fetch(apiUrl, { method: 'POST', headers: { @@ -477,13 +490,13 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, }); } else { const error = await response.text(); - console.error(error); + logger.error(error); throw new Error('Extension upload call query failed'); } } - console.debug('Made call to Microsoft.NAV.upload; response code: ', response.status); + logger.debug('Made call to Microsoft.NAV.upload; response code: ', response.status); } catch (err) { - console.error('Error during call: ', err.name, err.message); + logger.error('Error during call: ', err.name, err.message); throw err; } } @@ -493,7 +506,7 @@ async function waitForResponse(token, tenantId, environmentName, companyId, oper const startTimeStamp = Date.now(); let currentTimeStamp = Date.now(); - console.log(`Waiting an initial 2 seconds before polling...`); + logger.info(`Waiting an initial 2 seconds before polling...`); await sleep(2); let manualBreak = false; @@ -501,20 +514,20 @@ async function waitForResponse(token, tenantId, environmentName, companyId, oper let thisCheck = await getInstallationStatus(token, tenantId, environmentName, companyId, operationId); if (Array.isArray(thisCheck)) { if (thisCheck.length === 0) { - console.log('Received blank array back on extension installation status check; breaking'); - console.log('(This usually means that the upload call failed, and/or there are no other upload records in this instance of Business Central.)'); + logger.info('Received blank array back on extension installation status check; breaking'); + logger.info('(This usually means that the upload call failed, and/or there are no other upload records in this instance of Business Central.)'); manualBreak = true; } else { if (thisCheck[0].status !== 'InProgress') { - console.log(`Received status '${thisCheck[0].status}' response; breaking`); + logger.info(`Received status '${thisCheck[0].status}' response; breaking`); manualBreak = true; } else { - console.log(`Received status '${thisCheck[0].status}'; continuing to wait another ${waitTime} seconds`); + logger.info(`Received status '${thisCheck[0].status}'; continuing to wait another ${waitTime} seconds`); } } } - console.debug(Date.now(), ': checked progress, result:', thisCheck[0].status); + logger.debug(Date.now(), ': checked progress, result:', thisCheck[0].status); if (!parseBool(manualBreak)) { await sleep(waitTime) }; currentTimeStamp = Date.now(); } while ((((currentTimeStamp - startTimeStamp) / 1000) < maxWaitTime) && !parseBool(manualBreak)); @@ -549,5 +562,6 @@ module.exports = { uploadInstallationFile, callNavUploadCommand, waitForResponse, - parseBool + parseBool, + logger } \ No newline at end of file From 02409ba7f4496aec6008bb58919b363ec9ab64a6 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 17:09:07 -0600 Subject: [PATCH 085/130] More prettifying the logger --- bc-tools-extension/_common/CommonTools.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index ce77dfb..0409a2a 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -4,18 +4,31 @@ const { stat } = require('fs'); const testMode = process.env.INPUT_TESTMODE; +function escapeForVso(message) { + if (typeof message !== 'string') return ''; + + return message + .replace(/%/g, '%25') // MUST come first to avoid double escaping + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/\[/g, '%5B') + .replace(/\]/g, '%5D') + .replace(/'/g, ''') // optional: replace single quotes with HTML entity + .replace(/"/g, '"'); // optional: double quotes as well +} + const logger = { debug: async function (message) { - console.log(`##vso[task.debug]${message}`); + console.log(`##vso[task.debug]${escapeForVso(message)}`); }, info: async function (message) { console.log(message); }, warn: async function (message) { - console.log(`##vso[task.warning]${message}`); + console.log(`##vso[task.warning]${escapeForVso(message)}`); }, error: async function (message) { - console.log(`##vso[task.error]${message}`); + console.log(`##vso[task.error]${escapeForVso(message)}`); } }; From 15000e0c4c9b86539411836a34e2daeb17ffed43 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 17:30:23 -0600 Subject: [PATCH 086/130] Update to "new" logging standards --- bc-tools-extension/_common/CommonTools.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 0409a2a..708c89c 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -25,10 +25,10 @@ const logger = { console.log(message); }, warn: async function (message) { - console.log(`##vso[task.warning]${escapeForVso(message)}`); + console.log(`##vso[task.logissue type=warning]${escapeForVso(message)}`); }, error: async function (message) { - console.log(`##vso[task.error]${escapeForVso(message)}`); + console.log(`##vso[task.logissue type=error]${escapeForVso(message)}`); } }; From ee9be4aa8073f0a72c126bd8a165ae2693856806 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 17:32:51 -0600 Subject: [PATCH 087/130] Same thing --- bc-tools-extension/_common/CommonTools.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 708c89c..24cc1c4 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -13,8 +13,6 @@ function escapeForVso(message) { .replace(/\n/g, '%0A') .replace(/\[/g, '%5B') .replace(/\]/g, '%5D') - .replace(/'/g, ''') // optional: replace single quotes with HTML entity - .replace(/"/g, '"'); // optional: double quotes as well } const logger = { From 76c42293c9f7a58c150acf0f290c54145538708a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Fri, 6 Jun 2025 20:41:55 -0600 Subject: [PATCH 088/130] Logging corrections after the change to the logger --- .../function_Get-ListOfCompanies.js | 12 ++--- .../function_Get-ListOfModules.js | 2 +- .../function_Publish-BCModuleToTenant.js | 6 +-- bc-tools-extension/_common/CommonTools.js | 50 +++++++++---------- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js index 33bd2a7..b72d270 100644 --- a/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js +++ b/bc-tools-extension/Get-ListOfCompanies/function_Get-ListOfCompanies.js @@ -23,13 +23,13 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD logger.error('This is a logger.error command'.padStart(10)); logger.info(''); logger.info(''); - logger.debug('process.cwd():', process.cwd()); // where the process was started - logger.debug('__dirname:', __dirname); // where the current script file resides + logger.debug(`process.cwd(): ${process.cwd()}`); // where the process was started + logger.debug(`__dirname: ${__dirname}`); // where the current script file resides logger.debug('Enumerating parent:'); fs.readdir('..', (err, files) => { if (err) { - logger.error('Error reading directory: ', err); + logger.error(`Error reading directory: ${err}`); return; } logger.debug('Files in parent directory:'); @@ -41,7 +41,7 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD logger.debug('Enumerating current:'); fs.readdir('.', (err, files) => { if (err) { - logger.error('Error reading directory: ', err); + logger.error(`Error reading directory: ${err}`); return; } logger.debug('Files in parent directory:'); @@ -65,7 +65,7 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD logger.debug('**********************************************************'); const expectedPath = path.resolve(__dirname, '../_common/CommonTools.js'); - logger.debug('Trying to stat:', expectedPath); + logger.debug(`Trying to stat: ${expectedPath}`); try { fs.statSync(expectedPath); logger.debug('Found commonTools at expected path'); @@ -86,6 +86,6 @@ const extremeDebugMode = commonTools.parseBool(process.env.INPUT_EXTREMEDEBUGMOD }); } catch (error) { - logger.error('Error: ', error.message); + logger.error(`Error: ${error.message}`); } })(); \ No newline at end of file diff --git a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js index c65fa5d..42603f2 100644 --- a/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js +++ b/bc-tools-extension/Get-ListOfModules/function_Get-ListOfModules.js @@ -25,6 +25,6 @@ const excludeMicrosoft = process.env.INPUT_EXCLUDEMICROSOFT; logger.info(`${(idx + 1).toString().padStart(3)}. ${name.padEnd(60)} (Module ID: ${id}) v${version.padEnd(20)} (Package ID: ${pid})`); }); } catch (error) { - logger.error('Error: ', error.message); + logger.error(`Error: ${error.message}`); } })(); \ No newline at end of file diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index 117a3aa..b18a276 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -40,8 +40,8 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); extId = test.systemId; odata_etag = test['@odata.etag']; } - logger.debug('>>>>>>>>>> ExtId (the bookmark): ', extId); - logger.debug('>>>>>>>>>> @odata.etag: ', odata_etag); + logger.debug(`>>>>>>>>>> ExtId (the bookmark): ${extId}`); + logger.debug(`>>>>>>>>>> @odata.etag: ${odata_etag}`); logger.debug(''); logger.info('>>>>>>>>>> uploadInstallationFile'); let resulting = await commonTools.uploadInstallationFile(token, tenantId, environmentName, companyId, extId, filePath, odata_etag); @@ -56,6 +56,6 @@ const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); logger.info('>>>>>>>>>> done'); } catch (error) { - logger.error('Error: ', error.message); + logger.error(`Error: ${error.message}`); } })(); \ No newline at end of file diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 24cc1c4..3a3bf3e 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -126,7 +126,7 @@ async function getToken(tenantId, clientId, clientSecret) { }); if (!response.ok) { - logger.error('Failed to acquire token: ', response.status); + logger.error(`Failed to acquire token: ${response.status}`); const error = await response.text(); logger.error(error); throw new Error('Authentication failed'); @@ -155,7 +155,7 @@ async function getCompanies(token, tenantId, environmentName) { }); if (!response.ok) { - logger.error('Failed to get companies: ', response.status); + logger.error(`Failed to get companies: ${response.status}`); const error = await response.text(); logger.error(error); throw new Error('Company list query failed'); @@ -203,7 +203,7 @@ async function getModules(token, tenantId, environmentName, companyId, moduleId, }); if (!response.ok) { - logger.error('Failed to get modules: ', response.status); + logger.error(`Failed to get modules: ${response.status}`); const error = await response.text(); logger.error(error); throw new Error('Module list query failed'); @@ -246,7 +246,7 @@ async function confirmModule(token, tenantId, environmentName, companyId, module */ async function getInstallationStatus(token, tenantId, environmentName, companyId, operationId) { let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionDeploymentStatus`; - logger.debug('API (getInstallationStatus)', apiUrl); + logger.debug(`API (getInstallationStatus) ${apiUrl}`); const response = await fetch(apiUrl, { method: 'GET', @@ -257,7 +257,7 @@ async function getInstallationStatus(token, tenantId, environmentName, companyId }); if (!response.ok) { - logger.error('Failed to get extension deployments: ', response.status); + logger.error(`Failed to get extension deployments: ${response.status}`); const error = await response.text(); logger.error(error); throw new Error('Extension deployment status query failed'); @@ -311,7 +311,7 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp // This means, when returning the POST, the return is object.systemId, when returning the GET, the return is object[0].systemId let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload`; - logger.debug('API (createInstallationBookmark)', apiUrl); + logger.debug(`API (createInstallationBookmark) ${apiUrl}`); // per: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/resources/dynamics_extensionupload if (!schedule || schedule.trim() === "") { @@ -358,11 +358,11 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp errorResponse = await response.json(); } catch (e) { const raw = await response.text(); - logger.error('Non-JSON error response: ', raw); + logger.error(`Non-JSON error response: ${raw}`); throw new Error('Extension slot creation failed with unknown format'); } - logger.error('BC API error response: ', JSON.stringify(errorResponse, null, 2)); + logger.error(`BC API error response: ${JSON.stringify(errorResponse, null, 2)}`); if (status === 400 && errorResponse?.error?.code === "Internal_EntityWithSameKeyExists") { logger.warn('Extension upload already exists - retrieving existing record...'); @@ -407,8 +407,8 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp */ async function uploadInstallationFile(token, tenantId, environmentName, companyId, operationId, filePathAndName, odata_etag) { const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/extensionContent`; - logger.debug('API (uploadInstallationFile): ', apiUrl); - logger.debug('@odata.etag: ', odata_etag); + logger.debug(`API (uploadInstallationFile): ${apiUrl}`); + logger.debug(`@odata.etag: ${odata_etag}`); try { await fs.access(filePathAndName); @@ -419,12 +419,12 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI const fileBuffer = await fs.readFile(filePathAndName); logger.info(`Prepared file buffer of ${stats.size} bytes from file`); - logger.debug('Uploading file to: ', apiUrl); - logger.debug('Headers:', { + logger.debug(`Uploading file to: ${apiUrl}`); + logger.debug(`Headers: ${JSON.stringify({ 'Authorization': '[REDACTED]', 'Content-Type': 'application/octet-stream', 'If-Match': odata_etag - }); + }, null, 2)}`); const response = await fetch(apiUrl, { method: 'PATCH', @@ -439,12 +439,12 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI if (!response.ok) { const error = await response.text(); const errorCode = response.status; - logger.error('Upload failed: status code: ', errorCode); + logger.error(`Upload failed: status code: ${errorCode}`); logger.error(`Upload failed [${response.status}]: ${error}`); throw new Error('File upload failed.'); } - logger.info('Upload successful of:', filePathAndName, 'with a status code of:', response.status); + logger.info(`Upload successful of: ${filePathAndName} with a status code of: ${response.status}`); if (response.status === 204) { return true; @@ -454,9 +454,9 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI } catch (err) { if (err.code === 'ENOENT') { - logger.warn('File not found: ', filePathAndName); + logger.warn(`File not found: ${filePathAndName}`); } else { - logger.error('Unexpected error during upload: ', err); + logger.error(`Unexpected error during upload: ${err}`); } throw err; } @@ -464,8 +464,8 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI async function callNavUploadCommand(token, tenantId, environmentName, companyId, operationId, odata_etag) { const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/Microsoft.NAV.upload`; - logger.debug('API (callNavUploadCommand): ', apiUrl); - logger.debug('@odata.etag: ', odata_etag) + logger.debug(`API (callNavUploadCommand): ${apiUrl}`); + logger.debug(`@odata.etag: ${odata_etag}`) try { logger.info('Preparing to call Microsoft.NAV.upload'); @@ -480,17 +480,17 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, logger.info('Call to Microsoft.NAV.upload successful? ¯\\_(ツ)_/¯ It\'s not like Microsoft tells us...'); if (!response.ok) { - logger.error('Failed to call Microsoft.NAV.upload for deployment: ', response.status); + logger.error(`Failed to call Microsoft.NAV.upload for deployment: ${response.status}`); if (response.status === 409) { logger.info('Wait a second, that was a 409; this means that the @odata.etag is stale, let me try to get another one'); let refreshCheck = await createInstallationBookmark(token, tenantId, environmentName, companyId); - logger.info('Original odata.etag: ', odata_etag); + logger.info(`Original odata.etag: ${odata_etag}`); if (Array.isArray(refreshCheck)) { odata_etag = refreshCheck[0]['@odata.etag']; } else { odata_etag = refreshCheck['@odata.etag']; } - logger.info('Refreshed odata.etag:', odata_etag); + logger.info(`Refreshed odata.etag: ${odata_etag}`); response = await fetch(apiUrl, { method: 'POST', headers: { @@ -505,9 +505,9 @@ async function callNavUploadCommand(token, tenantId, environmentName, companyId, throw new Error('Extension upload call query failed'); } } - logger.debug('Made call to Microsoft.NAV.upload; response code: ', response.status); + logger.debug(`Made call to Microsoft.NAV.upload; response code: ${response.status}`); } catch (err) { - logger.error('Error during call: ', err.name, err.message); + logger.error(`Error during call: ${err.name} -- ${err.message}`); throw err; } } @@ -538,7 +538,7 @@ async function waitForResponse(token, tenantId, environmentName, companyId, oper } } } - logger.debug(Date.now(), ': checked progress, result:', thisCheck[0].status); + logger.debug(`${Date.now()}: checked progress, result: ${thisCheck[0].status}`); if (!parseBool(manualBreak)) { await sleep(waitTime) }; currentTimeStamp = Date.now(); } while ((((currentTimeStamp - startTimeStamp) / 1000) < maxWaitTime) && !parseBool(manualBreak)); From 5b169dfdff44a7220bfb2f21dc7af0984b8ffe30 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 9 Jun 2025 10:47:22 -0600 Subject: [PATCH 089/130] First pass at refactoring Build routine to JS --- _tasks/environments.json | 2 +- .../function_Build-ALPackage.js | 107 ++++++++++++++++++ bc-tools-extension/Build-ALPackage/task.json | 6 + 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js diff --git a/_tasks/environments.json b/_tasks/environments.json index 7c4081f..39e8fc5 100644 --- a/_tasks/environments.json +++ b/_tasks/environments.json @@ -2,7 +2,7 @@ "version": { "major": 0, "minor": 1, - "patch": 7, + "patch": 8, "build": 0 }, "dev": { diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js new file mode 100644 index 0000000..9fe3d7a --- /dev/null +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js @@ -0,0 +1,107 @@ +const { spawn } = require('child_process'); +const path = require('path'); +const os = require('os'); +const fs = require('fs'); +const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); + +// collect variables from input +const entireAppName = process.env.INPUT_ENTIREAPPNAME; +const baseProjectDirectory = process.env.INPUT_BASEPROJECTDIRECTORY; +const packagesDirectory = process.env.INPUT_PACKAGESDIRECTORY; +const outputDirectory = process.env.INPUT_OUTPUTDIRECTORY; +const alcPath = process.env.INPUT_ALEXEPATH; + +logger.info('Calling Build-ALPackage with the following parameters:'); +logger.info('EntireAppName'.padStart(2).padEnd(30) + entireAppName); +logger.info('BaseProjectDirectory'.padStart(2).padEnd(30) + baseProjectDirectory); +logger.info('PackagesDirectory'.padStart(2).padEnd(30) + packagesDirectory); +logger.info('OutputDirectory'.padStart(2).padEnd(30) + outputDirectory); +logger.info('ALEXEPath'.padStart(2).padEnd(30) + alcPath); +logger.info(''); + +// confirm all variables exist: +const requiredInputs = { entireAppName, baseProjectDirectory, packagesDirectory, outputDirectory, alcPath }; +for (const [key, value] of Object.entries(requiredInputs)) { + if (!value) { + logger.error(`Missing required input: ${key}`); + process.exit(1); + } +} + +// discover platform and environment +logger.debug(`Platform detected: ${os.platform()}`) +const isWindows = os.platform() === "win32"; +logger.debug(`isWindows: ${isWindows}`); +logger.debug(`Working directory: ${process.cwd()}`); + + +// check for existence and execution permissions on ALC +logger.debug('Checking for existence of ALC at target location'); +if (!fs.existsSync(alcPath)) { + logger.error(`ALC[.EXE] not found at ${alcPath}; terminating...`); + throw new Error(`ALC missing at path ${alcPath}`); +} + +try { + fs.accessSync(alcPath, fs.constants.X_OK); +} catch { + logger.warn('ALC was found, but not executable; attempting to apply execution permissions'); + try { + fs.chmodSync(alcPath, 0o755); + logger.warn(`Successfully applied execution privilege to ${alcPath}`); + } catch (err) { + logger.error(`Failed to set execution permission: ${err.message}`); + process.exit(1); + } +} + +// check for existence of packagesDirectory +logger.debug(`Checking for existence of packagesDirectory at ${packagesDirectory}`); +if (!fs.existsSync(packagesDirectory)) { + logger.error(`Didn't find anything at ${packagesDirectory}; terminating...`); + process.exit(1); +} + +// check for existence of output directory for the compiled package +logger.debug(`Checking for existence of outputDirectory at ${outputDirectory}`); +try { + fs.mkdirSync(outputDirectory, { recursive: true }); + logger.info(`Ensured output directory exists: ${outputDirectory}`); +} catch (err) { + logger.error(`Failed to create / confirm directory at ${outputDirectory}; failed with ${err.message}`); + process.exit(1); +} + +// build arguments for ALC +logger.debug(`Building Windows array?: ${isWindows}`); +const args = isWindows +? [ + `/project:${baseProjectDirectory}`, + `/out:${outputDirectory}`, + `/packageCachePath:${packagesDirectory}` +] +: [ + '--project', baseProjectDirectory, + '--out', outputDirectory, + '--packagecachepath', packagesDirectory +]; + +// attempt execution of ALC +logger.info(`Executing ALC with args:\n${[alcPath, ...args].map(x => `"${x}"`).join(' ')}`); + +const proc = spawn(alcPath, args, { stdio: 'inherit' }); + +proc.on('error', (err) => { + logger.error(`Spawn error: ${err.message}`); + process.exit(1); +}); + +proc.on('close', (code) => { + if (code !== 0) { + logger.warn(`ALC failed with exit code ${code}`); + process.exit(code); + } else { + logger.info('ALC compilation completed successfully'); + } +}); diff --git a/bc-tools-extension/Build-ALPackage/task.json b/bc-tools-extension/Build-ALPackage/task.json index 972976b..f3b2479 100644 --- a/bc-tools-extension/Build-ALPackage/task.json +++ b/bc-tools-extension/Build-ALPackage/task.json @@ -55,6 +55,12 @@ } ], "execution": { + "Node16": { + "target": "function_Build-ALPackage.js" + }, + "Node20_1": { + "target": "function_Build-ALPackage.js" + }, "PowerShell3": { "target": "wrapper_Build-ALPackage.ps1" } From a90a4bb76fdfdaeec581fadab4a7e48c9cd0cbad Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 9 Jun 2025 10:59:13 -0600 Subject: [PATCH 090/130] Dang it, forgot to add to the build script for the Build-ALPackage --- _tasks/_build.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_tasks/_build.ps1 b/_tasks/_build.ps1 index c60003a..370d82f 100644 --- a/_tasks/_build.ps1 +++ b/_tasks/_build.ps1 @@ -13,7 +13,8 @@ Write-Host "Copying _common contents to:" $paths = @( "./Get-ListOfCompanies", "./Get-ListOfModules", - "./Publish-BCModuleToTenant" + "./Publish-BCModuleToTenant", + "./Build-ALPackage" ) foreach($path in $paths) { From 305ebe4737292f47af47c58342c5861de81f5aff Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 9 Jun 2025 15:35:32 -0600 Subject: [PATCH 091/130] Try an agnostic setting for build --- .../function_Build-ALPackage.js | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js index 9fe3d7a..d3547a0 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js @@ -7,10 +7,10 @@ const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); // collect variables from input const entireAppName = process.env.INPUT_ENTIREAPPNAME; -const baseProjectDirectory = process.env.INPUT_BASEPROJECTDIRECTORY; -const packagesDirectory = process.env.INPUT_PACKAGESDIRECTORY; -const outputDirectory = process.env.INPUT_OUTPUTDIRECTORY; -const alcPath = process.env.INPUT_ALEXEPATH; +const baseProjectDirectory = process.env.INPUT_PROJECTPATH; +const packagesDirectory = process.env.INPUT_PACKAGECACHEPATH; +const outputDirectory = process.env.INPUT_OUTAPPFOLDER; +const alcPath = process.env.INPUT_ALEXEPATHFOLDER; logger.info('Calling Build-ALPackage with the following parameters:'); logger.info('EntireAppName'.padStart(2).padEnd(30) + entireAppName); @@ -35,14 +35,21 @@ const isWindows = os.platform() === "win32"; logger.debug(`isWindows: ${isWindows}`); logger.debug(`Working directory: ${process.cwd()}`); - // check for existence and execution permissions on ALC logger.debug('Checking for existence of ALC at target location'); if (!fs.existsSync(alcPath)) { - logger.error(`ALC[.EXE] not found at ${alcPath}; terminating...`); + logger.error(`ALC[.EXE] PATH not found at ${alcPath}; terminating...`); throw new Error(`ALC missing at path ${alcPath}`); } +if (!alcPath.toLowerCase().endsWith(isWindows ? 'alc.exe' : 'alc')) { + alcReference = path.join(alcPath, isWindows ? 'alc.exe' : 'alc'); + logger.debug(`Modified ALC path to include the executable: ${alcReference}`); +} else { + alcReference = alcPath; + logger.debug(`Using ALC path that includes the executable: ${alcReference}`); +} + try { fs.accessSync(alcPath, fs.constants.X_OK); } catch { @@ -90,7 +97,7 @@ const args = isWindows // attempt execution of ALC logger.info(`Executing ALC with args:\n${[alcPath, ...args].map(x => `"${x}"`).join(' ')}`); -const proc = spawn(alcPath, args, { stdio: 'inherit' }); +const proc = spawn(alcReference, args, { stdio: 'inherit' }); proc.on('error', (err) => { logger.error(`Spawn error: ${err.message}`); From 49f08a8d5d5d607fa943b1ddaa82a2925764c5c4 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 9 Jun 2025 15:54:11 -0600 Subject: [PATCH 092/130] Rebuild file name --- bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js index d3547a0..1e32564 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js @@ -81,6 +81,8 @@ try { } // build arguments for ALC +outputFile = path.join(outputDirectory, entireAppName); +logger.debug(`Specifying output as ${outputFile}`); logger.debug(`Building Windows array?: ${isWindows}`); const args = isWindows ? [ From a0c63d4190a2ad48940cec0d83d6a4a5eb2ce0c0 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 9 Jun 2025 16:14:30 -0600 Subject: [PATCH 093/130] Lazy-load undici only on calls that require it; correct file output naming --- .../function_Build-ALPackage.js | 5 +- bc-tools-extension/_common/CommonTools.js | 51 ++++++++++++------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js index 1e32564..82a0e9e 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js @@ -2,7 +2,6 @@ const { spawn } = require('child_process'); const path = require('path'); const os = require('os'); const fs = require('fs'); -const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); // collect variables from input @@ -87,12 +86,12 @@ logger.debug(`Building Windows array?: ${isWindows}`); const args = isWindows ? [ `/project:${baseProjectDirectory}`, - `/out:${outputDirectory}`, + `/out:${outputFile}`, `/packageCachePath:${packagesDirectory}` ] : [ '--project', baseProjectDirectory, - '--out', outputDirectory, + '--out', outputFile, '--packagecachepath', packagesDirectory ]; diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 3a3bf3e..fa03e2b 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -36,27 +36,35 @@ if (parseBool(testMode)) { } // this module uses undici for fetching specifically because the call to Microsoft.NAV.upload will return malformed and node-fetch can't parse it -let fetch; -try { - fetch = require('undici').fetch; -} catch (_) { - logger.warn("'undici' not found. Attempting to install..."); - const projectRoot = path.resolve(__dirname, '..'); - - const { execSync } = require('child_process'); - try { - execSync('npm install undici --no-progress --loglevel=warn', { - cwd: projectRoot, - stdio: 'inherit' - }); - fetch = require('undici').fetch; - } catch (installErr) { - console.error("Auto-install of 'undici' failed. Aborting."); - console.error(installErr); - process.exit(1); +let fetch = null; + +function usesUndici() { + if (!fetch) { + try { + fetch = require('undici').fetch; + } catch (_) { + logger.warn("'undici' not found. Attempting to install..."); + const projectRoot = path.resolve(__dirname, '..'); + + const { execSync } = require('child_process'); + try { + execSync('npm install undici --no-progress --loglevel=warn', { + cwd: projectRoot, + stdio: 'inherit' + }); + fetch = require('undici').fetch; + } catch (installErr) { + console.error("Auto-install of 'undici' failed. Aborting."); + console.error(installErr); + process.exit(1); + } + } } + return fetch; } + + /** * An obfuscation routine to block the client_secret in token request bodies * @param {string} body - the body of the token request, as a string @@ -109,6 +117,7 @@ function parseBool(val) { * @returns {string} a bearer token, if successful */ async function getToken(tenantId, clientId, clientSecret) { + usesUndici(); const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; const params = new URLSearchParams(); @@ -144,6 +153,7 @@ async function getToken(tenantId, clientId, clientSecret) { * @returns {object} a Business Central object, list of companies */ async function getCompanies(token, tenantId, environmentName) { + usesUndici(); const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/v2.0/companies`; const response = await fetch(apiUrl, { @@ -176,6 +186,7 @@ async function getCompanies(token, tenantId, environmentName) { * @returns {object} a Business Central object, list of modules installed */ async function getModules(token, tenantId, environmentName, companyId, moduleId, excludeMicrosoft) { + usesUndici(); let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensions`; const filters = []; @@ -245,6 +256,7 @@ async function confirmModule(token, tenantId, environmentName, companyId, module * @returns {object?} if successful, a response object; status is at .status */ async function getInstallationStatus(token, tenantId, environmentName, companyId, operationId) { + usesUndici(); let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionDeploymentStatus`; logger.debug(`API (getInstallationStatus) ${apiUrl}`); @@ -310,6 +322,7 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp // // This means, when returning the POST, the return is object.systemId, when returning the GET, the return is object[0].systemId + usesUndici(); let apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload`; logger.debug(`API (createInstallationBookmark) ${apiUrl}`); @@ -406,6 +419,7 @@ async function createInstallationBookmark(token, tenantId, environmentName, comp * @returns {boolean} true if successful; false if not */ async function uploadInstallationFile(token, tenantId, environmentName, companyId, operationId, filePathAndName, odata_etag) { + usesUndici(); const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/extensionContent`; logger.debug(`API (uploadInstallationFile): ${apiUrl}`); logger.debug(`@odata.etag: ${odata_etag}`); @@ -463,6 +477,7 @@ async function uploadInstallationFile(token, tenantId, environmentName, companyI } async function callNavUploadCommand(token, tenantId, environmentName, companyId, operationId, odata_etag) { + usesUndici(); const apiUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/api/microsoft/automation/v2.0/companies(${companyId})/extensionUpload(${operationId})/Microsoft.NAV.upload`; logger.debug(`API (callNavUploadCommand): ${apiUrl}`); logger.debug(`@odata.etag: ${odata_etag}`) From 7b8a8fcafe3c5cd007485529c0e5d449ae6887f6 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Mon, 9 Jun 2025 22:12:21 -0600 Subject: [PATCH 094/130] Add new function for get-bcdependencies --- .../function_Get-BCDependencies.js | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js new file mode 100644 index 0000000..e2abedc --- /dev/null +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js @@ -0,0 +1,208 @@ +const { spawn } = require('child_process'); +const path = require('path'); +const os = require('os'); +const fs = require('fs'); +const { PassThrough } = require('stream'); +const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { usesUndici } = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { fetch } = usesUndici(); + +// collect variables from input +const tenantId = process.env.INPUT_TENANTID; +const environmentName = process.env.INPUT_ENVIRONMENTNAME || 'sandbox'; +const clientId = process.env.INPUT_CLIENTID; +const clientSecret = process.env.INPUT_CLIENTSECRET; +const pathToAppJson = process.env.INPUT_PATHTOAPPJSON; +const pathToPackagesDirectory = process.env.INPUT_PATHTOPACKAGESDIRECTORY; +const testLoginOnly = process.env.INPUT_TESTLOGINONLY; +const skipDefaultDependencies = process.env.INPUT_SKIPDEFAULTDEPENDENCIES; + +logger.info('Calling Get-BCDependencies with the following parameters:'); +logger.info('TenantId'.padStart(2).padEnd(30) + tenantId); +logger.info('EnvironmentName'.padStart(2).padEnd(30) + environmentName); +logger.info('ClientId'.padStart(2).padEnd(30) + clientId); +logger.info('ClientSecret'.padStart(2).padEnd(30) + (clientSecret ? '[REDACTED]' : '[NOT PROVIDED]')); +logger.info('PathToAppJson'.padStart(2).padEnd(30) + pathToAppJson); +logger.info('PathToPackagesDirectory'.padStart(2).padEnd(30) + pathToPackagesDirectory); +logger.info('TestLoginOnly'.padStart(2).padEnd(30) + testLoginOnly); +logger.info('SkipDefaultDependencies'.padStart(2).padEnd(30) + skipDefaultDependencies); + +// confirm all variables exist: +const requiredInputs = { tenantId, environmentName, clientId, clientSecret, pathToAppJson, pathToPackagesDirectory, testLoginOnly, skipDefaultDependencies }; +for (const [key, value] of Object.entries(requiredInputs)) { + if (!value) { + logger.error(`Missing required input: ${key}`); + process.exit(1); + } +} + +// discover platform and environment +logger.debug(`Platform detected: ${os.platform()}`) +const isWindows = os.platform() === "win32"; +logger.debug(`isWindows: ${isWindows}`); +logger.debug(`Working directory: ${process.cwd()}`); + +// authenticate and break if "TestLoginOnly" +logger.info('>>>>>>>>>> getToken'); +const token = await commonTools.getToken(tenantId, clientId, clientSecret); + +if (commonTools.parseBool(testLoginOnly)) { + logger.info('Authenticated correctly; routine invoked with TestLoginOnly; terminating gracefully'); + process.exit(0); +} else { + logger.debug('Acquired token; moving on'); +} + +// confirm app.json exists +try { + let stat = fs.statSync(pathToAppJson) + if (stat.isDirectory()) { + pathToAppJson = path.join(pathToAppJson, 'app.json'); + logger.info(`Parsing path ${pathToAppJson}`); + } + stat = fs.statSync(pathToAppJson) + logger.info(`app.json found at ${pathToAppJson}`); +} catch (err) { + logger.error(`Invalid reference to app.json at ${pathToAppJson}`); + logger.error(`Error: ${err.message}`); + process.exit(1); +} + +// load app.json +let appJson; +try { + const appData = fs.readFileSync(pathToAppJson, 'utf8'); + appJson = JSON.parse(appData); + logger.info(`Loaded app.json from ${pathToAppJson}`); + logger.info(''); + logger.info('app.json enumeration:') + logger.info('App id:'.padStart(2).padEnd(15) + appJson.id); + logger.info('Publisher:'.padStart(2).padEnd(15) + appJson.publisher); + logger.info('Version:'.padStart(2).padEnd(15) + appJson.version); + logger.info(''); +} catch (err) { + logger.error(`Failed to load or parse app.json at ${pathToAppJson}`); + logger.error(`Error: ${err.message}`); + process.exit(1); +} + +// conditionally inject default dependencies +if (commonTools.parseBool(skipDefaultDependencies) === false) { + logger.debug('Adding dependencies; building list'); + const baseDependencies = [ + { + id: '63ca2fa4-4f03-4f2b-a480-172fef340d3f', + name: 'System Application', + publisher: 'Microsoft', + version: '' + }, + { + id: 'f3552374-a1f2-4356-848e-196002525837', + name: 'Business Foundation', + publisher: 'Microsoft', + version: '' + }, + { + id: '437dbf0e-84ff-417a-965d-ed2bb9650972', + name: 'Base Application', + publisher: 'Microsoft', + version: '' + }, + { + id: '6f2c034f-5ebe-4eae-b34c-90a0d4e87687', + name: '_Exclude_Business_Events_', + publisher: 'Microsoft', + version: '' + }, + { + id: '8874ed3a-0643-4247-9ced-7a7002f7135d', + name: 'System', + publisher: 'Microsoft', + version:'' + }, + { + id: '00000000-0000-0000-0000-000000000000', + name: 'Application', + publisher: 'Microsoft', + version: '' + } + ]; + + if (!Array.isArray(appJson.dependencies)) { + appJson.dependencies = []; + } + + for (const dep of baseDependencies) { + const exists = appJson.dependencies.some(d => d.id === dep.id || (d.name === dep.name && d.publisher === dep.publisher)); + if (!exists) { + logger.info(`Adding dependency ${dep.id} (${dep.name}) to list of dependencies`); + appJson.dependencies.push(dep); + } else { + logger.info(`Skipping addition of dependency ${dep.id} (${dep.name}) because it already exists`); + } + } +} else { + logger.info(`Skipping dependency additions because SkipDefaultDependencies is ${skipDefaultDependencies}`); +} + +// ensure packages directory exists or is created +try { + fs.mkdirSync(pathToPackagesDirectory, { recursive: true }); + logger.debug(`Confirmed or created the packages directory at ${pathToPackagesDirectory}`); +} catch (err) { + logger.error(`An error occurred trying to confirm or create ${pathToPackagesDirectory}`); + logger.error(`Error: ${err.message}`); + process.exit(1); +} + +// start to get the actual dependencies +for (const dep of appJson.dependencies) { + const siteId = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/dev/packages?publisher=${dep.publisher}&appName=${dep.name}&versionText=${dep.version}&appId=${dep.id}`; + logger.debug(`Endpoint: ${siteId}`); + try { + logger.info(`Downloading package ${dep.id} (${dep.name})`); + const response = await fetch(siteId, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/octet-stream' + } + }); + + if (!response.ok) { + logger.error(`An error occurred while trying to download ${dep.id} (${dep.name})`); + const text = await response.text(); + logger.debug(text); + throw new Error(`Download failed for ${dep.id} (${dep.name})`); + } + + const buffer = Buffer.from(await response.arrayBuffer()); + let filename = path.join(pathToPackagesDirectory, `${dep.name}.app`); + fs.writeFileSync(filename, buffer); + logger.debug(`File saved: ${filename}`); + } catch (err) { + logger.error(`An error occurred downloading ${dep.id} (${dep.name})`); + logger.error(`Error: ${err.message}`); + process.exit(1); + } +} + +console.info('Downloads complete'); + +if (!isWindows) { + logger.info(`Changing mod on ${pathToPackagesDirectory}`); + const stats = fs.statSync(pathToPackagesDirectory); + if (stats.isDirectory()) { + fs.chmodSync(pathToPackagesDirectory, 0o755); + for (const file of fs.readdirSync(pathToPackagesDirectory)) { + fs.chmodSync(path.join(pathToPackagesDirectory, file), 0o755); + logger.debug(`chmodded ${file}`); + } + } +} + +// enumerate folder for log +logger.info(`Files now in ${pathToPackagesDirectory}`); +for (const file of fs.readdirSync(pathToPackagesDirectory)) { + logger.info(file); +} From e7d613d5af2ce947c079026888940d4a577397af Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 06:26:16 -0600 Subject: [PATCH 095/130] Fix task.json for js reference --- bc-tools-extension/Get-BCDependencies/task.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bc-tools-extension/Get-BCDependencies/task.json b/bc-tools-extension/Get-BCDependencies/task.json index 674467b..1430f76 100644 --- a/bc-tools-extension/Get-BCDependencies/task.json +++ b/bc-tools-extension/Get-BCDependencies/task.json @@ -79,6 +79,12 @@ } ], "execution": { + "Node16": { + "target": "function_Get-BCDependencies.js" + }, + "Node20_1": { + "target": "function_Get-BCDependencies.js" + }, "PowerShell3": { "target": "wrapper_Get-BCDependencies.ps1" } From 2e61a48875dc38634e5d366b9d8689032b6126b6 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 06:32:12 -0600 Subject: [PATCH 096/130] Add get-bcdependencies to build script for commontools --- _tasks/_build.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_tasks/_build.ps1 b/_tasks/_build.ps1 index 370d82f..d81fdf8 100644 --- a/_tasks/_build.ps1 +++ b/_tasks/_build.ps1 @@ -14,7 +14,8 @@ $paths = @( "./Get-ListOfCompanies", "./Get-ListOfModules", "./Publish-BCModuleToTenant", - "./Build-ALPackage" + "./Build-ALPackage", + "./Get-BCDependencies" ) foreach($path in $paths) { From d282c52a5f3f536daba7008e94ad681988022ea3 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 08:13:03 -0600 Subject: [PATCH 097/130] Make command async to survive token acquisitions --- .../function_Get-BCDependencies.js | 364 +++++++++--------- 1 file changed, 183 insertions(+), 181 deletions(-) diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js index e2abedc..b7cb8f3 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js @@ -7,202 +7,204 @@ const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); const { usesUndici } = require(path.join(__dirname, '_common', 'CommonTools.js')); const { fetch } = usesUndici(); -// collect variables from input -const tenantId = process.env.INPUT_TENANTID; -const environmentName = process.env.INPUT_ENVIRONMENTNAME || 'sandbox'; -const clientId = process.env.INPUT_CLIENTID; -const clientSecret = process.env.INPUT_CLIENTSECRET; -const pathToAppJson = process.env.INPUT_PATHTOAPPJSON; -const pathToPackagesDirectory = process.env.INPUT_PATHTOPACKAGESDIRECTORY; -const testLoginOnly = process.env.INPUT_TESTLOGINONLY; -const skipDefaultDependencies = process.env.INPUT_SKIPDEFAULTDEPENDENCIES; - -logger.info('Calling Get-BCDependencies with the following parameters:'); -logger.info('TenantId'.padStart(2).padEnd(30) + tenantId); -logger.info('EnvironmentName'.padStart(2).padEnd(30) + environmentName); -logger.info('ClientId'.padStart(2).padEnd(30) + clientId); -logger.info('ClientSecret'.padStart(2).padEnd(30) + (clientSecret ? '[REDACTED]' : '[NOT PROVIDED]')); -logger.info('PathToAppJson'.padStart(2).padEnd(30) + pathToAppJson); -logger.info('PathToPackagesDirectory'.padStart(2).padEnd(30) + pathToPackagesDirectory); -logger.info('TestLoginOnly'.padStart(2).padEnd(30) + testLoginOnly); -logger.info('SkipDefaultDependencies'.padStart(2).padEnd(30) + skipDefaultDependencies); - -// confirm all variables exist: -const requiredInputs = { tenantId, environmentName, clientId, clientSecret, pathToAppJson, pathToPackagesDirectory, testLoginOnly, skipDefaultDependencies }; -for (const [key, value] of Object.entries(requiredInputs)) { - if (!value) { - logger.error(`Missing required input: ${key}`); - process.exit(1); - } -} - -// discover platform and environment -logger.debug(`Platform detected: ${os.platform()}`) -const isWindows = os.platform() === "win32"; -logger.debug(`isWindows: ${isWindows}`); -logger.debug(`Working directory: ${process.cwd()}`); - -// authenticate and break if "TestLoginOnly" -logger.info('>>>>>>>>>> getToken'); -const token = await commonTools.getToken(tenantId, clientId, clientSecret); - -if (commonTools.parseBool(testLoginOnly)) { - logger.info('Authenticated correctly; routine invoked with TestLoginOnly; terminating gracefully'); - process.exit(0); -} else { - logger.debug('Acquired token; moving on'); -} - -// confirm app.json exists -try { - let stat = fs.statSync(pathToAppJson) - if (stat.isDirectory()) { - pathToAppJson = path.join(pathToAppJson, 'app.json'); - logger.info(`Parsing path ${pathToAppJson}`); - } - stat = fs.statSync(pathToAppJson) - logger.info(`app.json found at ${pathToAppJson}`); -} catch (err) { - logger.error(`Invalid reference to app.json at ${pathToAppJson}`); - logger.error(`Error: ${err.message}`); - process.exit(1); -} - -// load app.json -let appJson; -try { - const appData = fs.readFileSync(pathToAppJson, 'utf8'); - appJson = JSON.parse(appData); - logger.info(`Loaded app.json from ${pathToAppJson}`); - logger.info(''); - logger.info('app.json enumeration:') - logger.info('App id:'.padStart(2).padEnd(15) + appJson.id); - logger.info('Publisher:'.padStart(2).padEnd(15) + appJson.publisher); - logger.info('Version:'.padStart(2).padEnd(15) + appJson.version); - logger.info(''); -} catch (err) { - logger.error(`Failed to load or parse app.json at ${pathToAppJson}`); - logger.error(`Error: ${err.message}`); - process.exit(1); -} - -// conditionally inject default dependencies -if (commonTools.parseBool(skipDefaultDependencies) === false) { - logger.debug('Adding dependencies; building list'); - const baseDependencies = [ - { - id: '63ca2fa4-4f03-4f2b-a480-172fef340d3f', - name: 'System Application', - publisher: 'Microsoft', - version: '' - }, - { - id: 'f3552374-a1f2-4356-848e-196002525837', - name: 'Business Foundation', - publisher: 'Microsoft', - version: '' - }, - { - id: '437dbf0e-84ff-417a-965d-ed2bb9650972', - name: 'Base Application', - publisher: 'Microsoft', - version: '' - }, - { - id: '6f2c034f-5ebe-4eae-b34c-90a0d4e87687', - name: '_Exclude_Business_Events_', - publisher: 'Microsoft', - version: '' - }, - { - id: '8874ed3a-0643-4247-9ced-7a7002f7135d', - name: 'System', - publisher: 'Microsoft', - version:'' - }, - { - id: '00000000-0000-0000-0000-000000000000', - name: 'Application', - publisher: 'Microsoft', - version: '' +async( () => { + // collect variables from input + const tenantId = process.env.INPUT_TENANTID; + const environmentName = process.env.INPUT_ENVIRONMENTNAME || 'sandbox'; + const clientId = process.env.INPUT_CLIENTID; + const clientSecret = process.env.INPUT_CLIENTSECRET; + const pathToAppJson = process.env.INPUT_PATHTOAPPJSON; + const pathToPackagesDirectory = process.env.INPUT_PATHTOPACKAGESDIRECTORY; + const testLoginOnly = process.env.INPUT_TESTLOGINONLY; + const skipDefaultDependencies = process.env.INPUT_SKIPDEFAULTDEPENDENCIES; + + logger.info('Calling Get-BCDependencies with the following parameters:'); + logger.info('TenantId'.padStart(2).padEnd(30) + tenantId); + logger.info('EnvironmentName'.padStart(2).padEnd(30) + environmentName); + logger.info('ClientId'.padStart(2).padEnd(30) + clientId); + logger.info('ClientSecret'.padStart(2).padEnd(30) + (clientSecret ? '[REDACTED]' : '[NOT PROVIDED]')); + logger.info('PathToAppJson'.padStart(2).padEnd(30) + pathToAppJson); + logger.info('PathToPackagesDirectory'.padStart(2).padEnd(30) + pathToPackagesDirectory); + logger.info('TestLoginOnly'.padStart(2).padEnd(30) + testLoginOnly); + logger.info('SkipDefaultDependencies'.padStart(2).padEnd(30) + skipDefaultDependencies); + + // confirm all variables exist: + const requiredInputs = { tenantId, environmentName, clientId, clientSecret, pathToAppJson, pathToPackagesDirectory, testLoginOnly, skipDefaultDependencies }; + for (const [key, value] of Object.entries(requiredInputs)) { + if (!value) { + logger.error(`Missing required input: ${key}`); + process.exit(1); } - ]; + } - if (!Array.isArray(appJson.dependencies)) { - appJson.dependencies = []; + // discover platform and environment + logger.debug(`Platform detected: ${os.platform()}`) + const isWindows = os.platform() === "win32"; + logger.debug(`isWindows: ${isWindows}`); + logger.debug(`Working directory: ${process.cwd()}`); + + // authenticate and break if "TestLoginOnly" + logger.info('>>>>>>>>>> getToken'); + const token = await commonTools.getToken(tenantId, clientId, clientSecret); + + if (commonTools.parseBool(testLoginOnly)) { + logger.info('Authenticated correctly; routine invoked with TestLoginOnly; terminating gracefully'); + process.exit(0); + } else { + logger.debug('Acquired token; moving on'); } - for (const dep of baseDependencies) { - const exists = appJson.dependencies.some(d => d.id === dep.id || (d.name === dep.name && d.publisher === dep.publisher)); - if (!exists) { - logger.info(`Adding dependency ${dep.id} (${dep.name}) to list of dependencies`); - appJson.dependencies.push(dep); - } else { - logger.info(`Skipping addition of dependency ${dep.id} (${dep.name}) because it already exists`); + // confirm app.json exists + try { + let stat = fs.statSync(pathToAppJson) + if (stat.isDirectory()) { + pathToAppJson = path.join(pathToAppJson, 'app.json'); + logger.info(`Parsing path ${pathToAppJson}`); } + stat = fs.statSync(pathToAppJson) + logger.info(`app.json found at ${pathToAppJson}`); + } catch (err) { + logger.error(`Invalid reference to app.json at ${pathToAppJson}`); + logger.error(`Error: ${err.message}`); + process.exit(1); } -} else { - logger.info(`Skipping dependency additions because SkipDefaultDependencies is ${skipDefaultDependencies}`); -} - -// ensure packages directory exists or is created -try { - fs.mkdirSync(pathToPackagesDirectory, { recursive: true }); - logger.debug(`Confirmed or created the packages directory at ${pathToPackagesDirectory}`); -} catch (err) { - logger.error(`An error occurred trying to confirm or create ${pathToPackagesDirectory}`); - logger.error(`Error: ${err.message}`); - process.exit(1); -} - -// start to get the actual dependencies -for (const dep of appJson.dependencies) { - const siteId = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/dev/packages?publisher=${dep.publisher}&appName=${dep.name}&versionText=${dep.version}&appId=${dep.id}`; - logger.debug(`Endpoint: ${siteId}`); + + // load app.json + let appJson; try { - logger.info(`Downloading package ${dep.id} (${dep.name})`); - const response = await fetch(siteId, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/octet-stream' + const appData = fs.readFileSync(pathToAppJson, 'utf8'); + appJson = JSON.parse(appData); + logger.info(`Loaded app.json from ${pathToAppJson}`); + logger.info(''); + logger.info('app.json enumeration:') + logger.info('App id:'.padStart(2).padEnd(15) + appJson.id); + logger.info('Publisher:'.padStart(2).padEnd(15) + appJson.publisher); + logger.info('Version:'.padStart(2).padEnd(15) + appJson.version); + logger.info(''); + } catch (err) { + logger.error(`Failed to load or parse app.json at ${pathToAppJson}`); + logger.error(`Error: ${err.message}`); + process.exit(1); + } + + // conditionally inject default dependencies + if (commonTools.parseBool(skipDefaultDependencies) === false) { + logger.debug('Adding dependencies; building list'); + const baseDependencies = [ + { + id: '63ca2fa4-4f03-4f2b-a480-172fef340d3f', + name: 'System Application', + publisher: 'Microsoft', + version: '' + }, + { + id: 'f3552374-a1f2-4356-848e-196002525837', + name: 'Business Foundation', + publisher: 'Microsoft', + version: '' + }, + { + id: '437dbf0e-84ff-417a-965d-ed2bb9650972', + name: 'Base Application', + publisher: 'Microsoft', + version: '' + }, + { + id: '6f2c034f-5ebe-4eae-b34c-90a0d4e87687', + name: '_Exclude_Business_Events_', + publisher: 'Microsoft', + version: '' + }, + { + id: '8874ed3a-0643-4247-9ced-7a7002f7135d', + name: 'System', + publisher: 'Microsoft', + version:'' + }, + { + id: '00000000-0000-0000-0000-000000000000', + name: 'Application', + publisher: 'Microsoft', + version: '' } - }); + ]; - if (!response.ok) { - logger.error(`An error occurred while trying to download ${dep.id} (${dep.name})`); - const text = await response.text(); - logger.debug(text); - throw new Error(`Download failed for ${dep.id} (${dep.name})`); + if (!Array.isArray(appJson.dependencies)) { + appJson.dependencies = []; } - const buffer = Buffer.from(await response.arrayBuffer()); - let filename = path.join(pathToPackagesDirectory, `${dep.name}.app`); - fs.writeFileSync(filename, buffer); - logger.debug(`File saved: ${filename}`); + for (const dep of baseDependencies) { + const exists = appJson.dependencies.some(d => d.id === dep.id || (d.name === dep.name && d.publisher === dep.publisher)); + if (!exists) { + logger.info(`Adding dependency ${dep.id} (${dep.name}) to list of dependencies`); + appJson.dependencies.push(dep); + } else { + logger.info(`Skipping addition of dependency ${dep.id} (${dep.name}) because it already exists`); + } + } + } else { + logger.info(`Skipping dependency additions because SkipDefaultDependencies is ${skipDefaultDependencies}`); + } + + // ensure packages directory exists or is created + try { + fs.mkdirSync(pathToPackagesDirectory, { recursive: true }); + logger.debug(`Confirmed or created the packages directory at ${pathToPackagesDirectory}`); } catch (err) { - logger.error(`An error occurred downloading ${dep.id} (${dep.name})`); + logger.error(`An error occurred trying to confirm or create ${pathToPackagesDirectory}`); logger.error(`Error: ${err.message}`); process.exit(1); } -} - -console.info('Downloads complete'); - -if (!isWindows) { - logger.info(`Changing mod on ${pathToPackagesDirectory}`); - const stats = fs.statSync(pathToPackagesDirectory); - if (stats.isDirectory()) { - fs.chmodSync(pathToPackagesDirectory, 0o755); - for (const file of fs.readdirSync(pathToPackagesDirectory)) { - fs.chmodSync(path.join(pathToPackagesDirectory, file), 0o755); - logger.debug(`chmodded ${file}`); + + // start to get the actual dependencies + for (const dep of appJson.dependencies) { + const siteId = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environmentName}/dev/packages?publisher=${dep.publisher}&appName=${dep.name}&versionText=${dep.version}&appId=${dep.id}`; + logger.debug(`Endpoint: ${siteId}`); + try { + logger.info(`Downloading package ${dep.id} (${dep.name})`); + const response = await fetch(siteId, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/octet-stream' + } + }); + + if (!response.ok) { + logger.error(`An error occurred while trying to download ${dep.id} (${dep.name})`); + const text = await response.text(); + logger.debug(text); + throw new Error(`Download failed for ${dep.id} (${dep.name})`); + } + + const buffer = Buffer.from(await response.arrayBuffer()); + let filename = path.join(pathToPackagesDirectory, `${dep.name}.app`); + fs.writeFileSync(filename, buffer); + logger.debug(`File saved: ${filename}`); + } catch (err) { + logger.error(`An error occurred downloading ${dep.id} (${dep.name})`); + logger.error(`Error: ${err.message}`); + process.exit(1); + } + } + + console.info('Downloads complete'); + + if (!isWindows) { + logger.info(`Changing mod on ${pathToPackagesDirectory}`); + const stats = fs.statSync(pathToPackagesDirectory); + if (stats.isDirectory()) { + fs.chmodSync(pathToPackagesDirectory, 0o755); + for (const file of fs.readdirSync(pathToPackagesDirectory)) { + fs.chmodSync(path.join(pathToPackagesDirectory, file), 0o755); + logger.debug(`chmodded ${file}`); + } } } -} -// enumerate folder for log -logger.info(`Files now in ${pathToPackagesDirectory}`); -for (const file of fs.readdirSync(pathToPackagesDirectory)) { - logger.info(file); -} + // enumerate folder for log + logger.info(`Files now in ${pathToPackagesDirectory}`); + for (const file of fs.readdirSync(pathToPackagesDirectory)) { + logger.info(file); + } +})(); \ No newline at end of file From 587fb8e425fd12398f508807f1f072b913724fc5 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 08:28:06 -0600 Subject: [PATCH 098/130] Messed up the async call --- .../Get-BCDependencies/function_Get-BCDependencies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js index b7cb8f3..75af251 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js @@ -7,7 +7,7 @@ const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); const { usesUndici } = require(path.join(__dirname, '_common', 'CommonTools.js')); const { fetch } = usesUndici(); -async( () => { +(async () => { // collect variables from input const tenantId = process.env.INPUT_TENANTID; const environmentName = process.env.INPUT_ENVIRONMENTNAME || 'sandbox'; From dfba6d7abc798520ad58431c31936948ffe3b141 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 08:33:29 -0600 Subject: [PATCH 099/130] Bad call to commonTools --- .../Get-BCDependencies/function_Get-BCDependencies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js index 75af251..086a4be 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js @@ -5,7 +5,7 @@ const fs = require('fs'); const { PassThrough } = require('stream'); const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); const { usesUndici } = require(path.join(__dirname, '_common', 'CommonTools.js')); -const { fetch } = usesUndici(); +const { fetch } = commonTools.usesUndici(); (async () => { // collect variables from input From cf46f7cdb94e7fd24e1963990cfcdd6840e0551f Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 08:40:43 -0600 Subject: [PATCH 100/130] Remove function type from usesUndici; not sure this is going to work --- .../Get-BCDependencies/function_Get-BCDependencies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js index 086a4be..ed451f3 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js @@ -5,7 +5,7 @@ const fs = require('fs'); const { PassThrough } = require('stream'); const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); const { usesUndici } = require(path.join(__dirname, '_common', 'CommonTools.js')); -const { fetch } = commonTools.usesUndici(); +const { fetch } = usesUndici; (async () => { // collect variables from input From 34260ddca58153b569a3f56f48451168b6568aa1 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 09:07:32 -0600 Subject: [PATCH 101/130] Adding filename capture; locally debugged version --- .../function_Get-BCDependencies.js | 36 +++++++++++-------- bc-tools-extension/_common/CommonTools.js | 3 +- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js index ed451f3..569ee17 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js @@ -3,20 +3,19 @@ const path = require('path'); const os = require('os'); const fs = require('fs'); const { PassThrough } = require('stream'); -const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); -const { usesUndici } = require(path.join(__dirname, '_common', 'CommonTools.js')); -const { fetch } = usesUndici; +const { usesUndici, logger, parseBool, getToken } = require(path.join(__dirname, '_common', 'CommonTools.js')); +const fetch = usesUndici(); (async () => { // collect variables from input - const tenantId = process.env.INPUT_TENANTID; - const environmentName = process.env.INPUT_ENVIRONMENTNAME || 'sandbox'; - const clientId = process.env.INPUT_CLIENTID; - const clientSecret = process.env.INPUT_CLIENTSECRET; - const pathToAppJson = process.env.INPUT_PATHTOAPPJSON; - const pathToPackagesDirectory = process.env.INPUT_PATHTOPACKAGESDIRECTORY; - const testLoginOnly = process.env.INPUT_TESTLOGINONLY; - const skipDefaultDependencies = process.env.INPUT_SKIPDEFAULTDEPENDENCIES; + const tenantId = process.env.INPUT_TENANTID; + const environmentName = process.env.INPUT_ENVIRONMENTNAME || 'sandbox'; + const clientId = process.env.INPUT_CLIENTID; + const clientSecret = process.env.INPUT_CLIENTSECRET; + let pathToAppJson = process.env.INPUT_PATHTOAPPJSON; + const pathToPackagesDirectory = process.env.INPUT_PATHTOPACKAGESDIRECTORY; + const testLoginOnly = process.env.INPUT_TESTLOGINONLY; + const skipDefaultDependencies = process.env.INPUT_SKIPDEFAULTDEPENDENCIES; logger.info('Calling Get-BCDependencies with the following parameters:'); logger.info('TenantId'.padStart(2).padEnd(30) + tenantId); @@ -45,9 +44,9 @@ const { fetch } = usesUndici; // authenticate and break if "TestLoginOnly" logger.info('>>>>>>>>>> getToken'); - const token = await commonTools.getToken(tenantId, clientId, clientSecret); + const token = await getToken(tenantId, clientId, clientSecret); - if (commonTools.parseBool(testLoginOnly)) { + if (parseBool(testLoginOnly)) { logger.info('Authenticated correctly; routine invoked with TestLoginOnly; terminating gracefully'); process.exit(0); } else { @@ -88,7 +87,7 @@ const { fetch } = usesUndici; } // conditionally inject default dependencies - if (commonTools.parseBool(skipDefaultDependencies) === false) { + if (parseBool(skipDefaultDependencies) === false) { logger.debug('Adding dependencies; building list'); const baseDependencies = [ { @@ -177,8 +176,15 @@ const { fetch } = usesUndici; throw new Error(`Download failed for ${dep.id} (${dep.name})`); } - const buffer = Buffer.from(await response.arrayBuffer()); + const contentDisposition = response.headers.get('content-disposition'); let filename = path.join(pathToPackagesDirectory, `${dep.name}.app`); + if (contentDisposition) { + const match = /filename\*?=(?:UTF-8''|")?([^;"\n]+)/i.exec(contentDisposition); + if (match && match[1]) { + filename = path.join(pathToPackagesDirectory, decodeURIComponent(match[1].replace(/"/g, ''))); + } + } + const buffer = Buffer.from(await response.arrayBuffer()); fs.writeFileSync(filename, buffer); logger.debug(`File saved: ${filename}`); } catch (err) { diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index fa03e2b..03abea4 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -589,5 +589,6 @@ module.exports = { callNavUploadCommand, waitForResponse, parseBool, - logger + logger, + usesUndici } \ No newline at end of file From 48ae495441a5a61d0a74c442b7be5d23e3082cf1 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 13:02:31 -0600 Subject: [PATCH 102/130] Migrate VSIX acquisition to JS --- _tasks/_build.ps1 | 3 +- .../function_Get-VSIXCompiler.js | 179 ++++++++++++++++++ bc-tools-extension/package-lock.json | 138 +++++++++++++- bc-tools-extension/package.json | 3 +- 4 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js diff --git a/_tasks/_build.ps1 b/_tasks/_build.ps1 index d81fdf8..84ca8be 100644 --- a/_tasks/_build.ps1 +++ b/_tasks/_build.ps1 @@ -15,7 +15,8 @@ $paths = @( "./Get-ListOfModules", "./Publish-BCModuleToTenant", "./Build-ALPackage", - "./Get-BCDependencies" + "./Get-BCDependencies", + "./Get-VSIXCompiler" ) foreach($path in $paths) { diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js new file mode 100644 index 0000000..1f1cea2 --- /dev/null +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js @@ -0,0 +1,179 @@ +const { spawn } = require('child_process'); +const path = require('path'); +const os = require('os'); +const fs = require('fs'); +const { PassThrough } = require('stream'); +const { usesUndici, logger, parseBool, getToken } = require(path.join(__dirname, '_common', 'CommonTools.js')); +const fetch = usesUndici(); +const crypto = require('crypto'); +const { pipeline } = require('stream/promises'); + +(async () => { + // collect variables from input + const downloadDirectory = process.env.INPUT_DOWNLOADDIRECTORY; + const downloadVersion = process.env.INPUT_VERSION; + + logger.info('Calling Get-VSIXCompiler with the following parameters:'); + logger.info('DownloadDirectory'.padStart(2).padEnd(30) + downloadDirectory); + logger.info('Version'.padStart(2).padEnd(30) + downloadVersion); + + // confirm all variables exist + const requiredInputs = { downloadDirectory, downloadVersion }; + for (const [key, value] of Object.entries(requiredInputs)) { + if (!value) { + logger.error(`Missing required input: ${key}`); + process.exit(1); + } + } + + // confirm the download directory exists + try { + fs.mkdirSync(downloadDirectory, { recursive: true }); + logger.debug(`Confirmed or created the download directory at ${downloadDirectory}`); + } catch (err) { + logger.error(`An error occurred trying to confirm or create ${downloadDirectory}`); + logger.error(`Error: ${err.message}`); + process.exit(1); + } + + // discover platform and environment + logger.debug(`Platform detected: ${os.platform()}`) + const isWindows = os.platform() === "win32"; + logger.debug(`isWindows: ${isWindows}`); + logger.debug(`Working directory: ${process.cwd()}`); + + // find latest (or specified) version + let apiUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery"; + logger.debug(`Contacting ${apiUrl}`); + + const jsonRawPrototype = { + filters: [ + { + criteria: [ + { + filterType: 7, + value: "ms-dynamics-smb.al" + } + ], + pageNumber: 1, + pageSize: 100, + sortBy: 0, + sortOrder: 0 + } + ], + assetTypes: [], + flags: 129 + }; + + logger.debug(`JSON body: ${JSON.stringify(jsonRawPrototype)}`); + + let versionResult; + + try { + const versionResponse = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Accept': 'application/json; charset=utf-8;api-version=7.2-preview.1', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(jsonRawPrototype) + }); + + if (!versionResponse.ok) { + logger.error('Something went wrong getting the version information'); + const text = await versionResponse.text(); + logger.debug(text); + throw new Error('Version check failed'); + } + + versionResult = await versionResponse.json(); + } catch (err) { + logger.error('Something went wrong while trying to get version information'); + logger.error(`Error: ${err.message}`); + process.exit(1); + } + + // prepare download link from information retrieved from the API + const publisher = versionResult.results[0].extensions[0].publisher.publisherName; + const extension = versionResult.results[0].extensions[0].extensionName; + logger.info(`Received response from the API with ${versionResult.results[0].extensions[0].versions.length} versions for ${extension} by ${publisher}`); + + let version; + if (downloadVersion === 'latest') { + version = versionResult.results[0].extensions[0].versions[0].version; + } else { + const checkVersions = versionResult.results?.[0]?.extensions?.[0]?.versions ?? []; + const versionExists = checkVersions.some(v => v.version === downloadVersion); + if (versionExists) { + version = downloadVersion; + logger.info(`Confirmed version ${version} exists`); + } else { + logger.error(`Version ${downloadVersion} does not exist in the results; please verify and try again`); + process.exit(1); + } + } + + logger.info(''); + logger.info('Acquiring compiler with the follow attributes:'); + logger.info('publisher'.padStart(2).padEnd(15) + publisher); + logger.info('extension'.padStart(2).padEnd(15) + extension); + logger.info('version'.padStart(2).padEnd(15) + version); + logger.info(''); + + const downloadUrl = `https://${publisher}.gallery.vsassets.io/_apis/public/gallery/publisher/${publisher}/extension/${extension}/${version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage`; + logger.debug(`Acquisition url: ${downloadUrl}`); + + // attempt download + const downloadFilename = path.join(downloadDirectory, 'compiler.vsix'); + + try { + const downloadResponse = await fetch(downloadUrl); + + if (!downloadResponse.ok) { + logger.error(`Something went wrong downloading the compiler; got response code ${downloadResponse.status}: ${downloadResponse.statusText}`); + process.exit(1); + } + + // note: content-disposition does not contain a particularly logical, valid or stable filename... + await pipeline(downloadResponse.body, fs.createWriteStream(downloadFilename)); + logger.info(`Downloaded file: ${downloadFilename}`); + + const fileBuffer = await fs.promises.readFile(downloadFilename); + const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + logger.info(`SHA256: ${hash}`); + + } catch (err) { + logger.error('Something went wrong downloading the compiler'); + logger.error(`Error: ${err.message}`); + process.exit(1); + } + + // attempt to install unzipper + let unzipper; + + try { + unzipper = require('unzipper'); + } catch (_) { + logger.info('Unzipper not found; attempting auto install'); + const projectRoot = path.resolve(__dirname, '..'); + const { execSync } = require('child_process'); + try { + execSync('npm install unzipper --no-progress --loglevel=warn', { + cwd: projectRoot, + stdio: 'inherit' + }); + unzipper = require('unzipper'); + } catch (installErr) { + logger.error('Could not automatically acquire unzipper'); + logger.error(`Error: ${err.message}`); + process.exit(1); + } + } + + // decompile + const extractTo = path.join(downloadDirectory, 'expanded'); + + logger.info(`Extracting ${downloadFilename} to ${extractTo}`); + await fs.createReadStream(downloadFilename).pipe(unzipper.Extract( {path: extractTo })).promise(); + logger.info(`Extracted ${downloadFilename} to ${extractTo}`); +})(); \ No newline at end of file diff --git a/bc-tools-extension/package-lock.json b/bc-tools-extension/package-lock.json index 813afe5..16cf68a 100644 --- a/bc-tools-extension/package-lock.json +++ b/bc-tools-extension/package-lock.json @@ -6,7 +6,73 @@ "": { "dependencies": { "node-fetch": "^2.7.0", - "undici": "^7.10.0" + "undici": "^7.10.0", + "unzipper": "^0.12.3" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, "node_modules/node-fetch": { @@ -29,6 +95,48 @@ } } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -44,6 +152,34 @@ "node": ">=20.18.1" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/bc-tools-extension/package.json b/bc-tools-extension/package.json index 088ae6c..4280478 100644 --- a/bc-tools-extension/package.json +++ b/bc-tools-extension/package.json @@ -1,6 +1,7 @@ { "dependencies": { "node-fetch": "^2.7.0", - "undici": "^7.10.0" + "undici": "^7.10.0", + "unzipper": "^0.12.3" } } From 588ee90676ade4ec24302d7dedc1464ac3df40c4 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 13:03:31 -0600 Subject: [PATCH 103/130] Update task.json for VSIX --- bc-tools-extension/Get-VSIXCompiler/task.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bc-tools-extension/Get-VSIXCompiler/task.json b/bc-tools-extension/Get-VSIXCompiler/task.json index 17de076..568669d 100644 --- a/bc-tools-extension/Get-VSIXCompiler/task.json +++ b/bc-tools-extension/Get-VSIXCompiler/task.json @@ -31,6 +31,12 @@ } ], "execution": { + "Node16": { + "target": "function_Get-VSIXCompiler.js" + }, + "Node20_1": { + "target": "function_Get-VSIXCompiler.js" + }, "PowerShell3": { "target": "wrapper_Get-VSIXCompiler.ps1" } From d557a0c3ccddb2dfbc50968b7f2f28e99ae836e9 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 13:20:45 -0600 Subject: [PATCH 104/130] Remove Powershell leftovers --- .../function_Build-ALPackage.ps1 | 124 --- .../ps_modules/VstsTaskSdk/FindFunctions.ps1 | 728 ------------------ .../ps_modules/VstsTaskSdk/InputFunctions.ps1 | 524 ------------- .../VstsTaskSdk/LegacyFindFunctions.ps1 | 320 -------- .../VstsTaskSdk/LocalizationFunctions.ps1 | 150 ---- .../VstsTaskSdk/LoggingCommandFunctions.ps1 | 634 --------------- .../VstsTaskSdk/LongPathFunctions.ps1 | 205 ----- .../ps_modules/VstsTaskSdk/Minimatch.dll | Bin 18432 -> 0 bytes .../ps_modules/VstsTaskSdk/OutFunctions.ps1 | 79 -- .../VstsTaskSdk/PSGetModuleInfo.xml | 117 --- .../VstsTaskSdk/ServerOMFunctions.ps1 | 684 ---------------- .../resources.resjson/de-DE/resources.resjson | 18 - .../resources.resjson/en-US/resources.resjson | 18 - .../resources.resjson/es-ES/resources.resjson | 18 - .../resources.resjson/fr-FR/resources.resjson | 18 - .../resources.resjson/it-IT/resources.resjson | 18 - .../resources.resjson/ja-JP/resources.resjson | 18 - .../resources.resjson/ko-KR/resources.resjson | 18 - .../resources.resjson/ru-RU/resources.resjson | 18 - .../resources.resjson/zh-CN/resources.resjson | 18 - .../resources.resjson/zh-TW/resources.resjson | 18 - .../ps_modules/VstsTaskSdk/ToolFunctions.ps1 | 227 ------ .../ps_modules/VstsTaskSdk/TraceFunctions.ps1 | 139 ---- .../ps_modules/VstsTaskSdk/VstsTaskSdk.dll | Bin 25408 -> 0 bytes .../ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 | 22 - .../ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 | 181 ----- .../ps_modules/VstsTaskSdk/lib.json | 20 - bc-tools-extension/Build-ALPackage/task.json | 5 +- .../wrapper_Build-ALPackage.ps1 | 16 - .../Tests.Add-BaseDependenciesIfMissing.ps1 | 59 -- ...function_Add-BaseDependenciesIfMissing.ps1 | 79 -- .../function_Get-BCDependencies.ps1 | 229 ------ .../ps_modules/VstsTaskSdk/FindFunctions.ps1 | 728 ------------------ .../ps_modules/VstsTaskSdk/InputFunctions.ps1 | 524 ------------- .../VstsTaskSdk/LegacyFindFunctions.ps1 | 320 -------- .../VstsTaskSdk/LocalizationFunctions.ps1 | 150 ---- .../VstsTaskSdk/LoggingCommandFunctions.ps1 | 634 --------------- .../VstsTaskSdk/LongPathFunctions.ps1 | 205 ----- .../ps_modules/VstsTaskSdk/Minimatch.dll | Bin 18432 -> 0 bytes .../ps_modules/VstsTaskSdk/OutFunctions.ps1 | 79 -- .../VstsTaskSdk/PSGetModuleInfo.xml | 117 --- .../VstsTaskSdk/ServerOMFunctions.ps1 | 684 ---------------- .../resources.resjson/de-DE/resources.resjson | 18 - .../resources.resjson/en-US/resources.resjson | 18 - .../resources.resjson/es-ES/resources.resjson | 18 - .../resources.resjson/fr-FR/resources.resjson | 18 - .../resources.resjson/it-IT/resources.resjson | 18 - .../resources.resjson/ja-JP/resources.resjson | 18 - .../resources.resjson/ko-KR/resources.resjson | 18 - .../resources.resjson/ru-RU/resources.resjson | 18 - .../resources.resjson/zh-CN/resources.resjson | 18 - .../resources.resjson/zh-TW/resources.resjson | 18 - .../ps_modules/VstsTaskSdk/ToolFunctions.ps1 | 227 ------ .../ps_modules/VstsTaskSdk/TraceFunctions.ps1 | 139 ---- .../ps_modules/VstsTaskSdk/VstsTaskSdk.dll | Bin 25408 -> 0 bytes .../ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 | 22 - .../ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 | 181 ----- .../ps_modules/VstsTaskSdk/lib.json | 20 - .../Get-BCDependencies/task.json | 3 - .../wrapper_Get-BCDependencies.ps1 | 40 - .../function_Get-VSIXCompiler.ps1 | 214 ----- .../ps_modules/VstsTaskSdk/FindFunctions.ps1 | 728 ------------------ .../ps_modules/VstsTaskSdk/InputFunctions.ps1 | 524 ------------- .../VstsTaskSdk/LegacyFindFunctions.ps1 | 320 -------- .../VstsTaskSdk/LocalizationFunctions.ps1 | 150 ---- .../VstsTaskSdk/LoggingCommandFunctions.ps1 | 634 --------------- .../VstsTaskSdk/LongPathFunctions.ps1 | 205 ----- .../ps_modules/VstsTaskSdk/Minimatch.dll | Bin 18432 -> 0 bytes .../ps_modules/VstsTaskSdk/OutFunctions.ps1 | 79 -- .../VstsTaskSdk/PSGetModuleInfo.xml | 117 --- .../VstsTaskSdk/ServerOMFunctions.ps1 | 684 ---------------- .../resources.resjson/de-DE/resources.resjson | 18 - .../resources.resjson/en-US/resources.resjson | 18 - .../resources.resjson/es-ES/resources.resjson | 18 - .../resources.resjson/fr-FR/resources.resjson | 18 - .../resources.resjson/it-IT/resources.resjson | 18 - .../resources.resjson/ja-JP/resources.resjson | 18 - .../resources.resjson/ko-KR/resources.resjson | 18 - .../resources.resjson/ru-RU/resources.resjson | 18 - .../resources.resjson/zh-CN/resources.resjson | 18 - .../resources.resjson/zh-TW/resources.resjson | 18 - .../ps_modules/VstsTaskSdk/ToolFunctions.ps1 | 227 ------ .../ps_modules/VstsTaskSdk/TraceFunctions.ps1 | 139 ---- .../ps_modules/VstsTaskSdk/VstsTaskSdk.dll | Bin 25408 -> 0 bytes .../ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 | 22 - .../ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 | 181 ----- .../ps_modules/VstsTaskSdk/lib.json | 20 - bc-tools-extension/Get-VSIXCompiler/task.json | 3 - .../wrapper_Get-VSIXCompiler.ps1 | 41 - bc-tools-extension/_common/CommonTools.ps1 | 29 - 90 files changed, 1 insertion(+), 13471 deletions(-) delete mode 100644 bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/FindFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/InputFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Minimatch.dll delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/OutFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/ToolFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/TraceFunctions.ps1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/VstsTaskSdk.dll delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 delete mode 100644 bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/lib.json delete mode 100644 bc-tools-extension/Build-ALPackage/wrapper_Build-ALPackage.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/Tests/Unit/Tests.Add-BaseDependenciesIfMissing.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/function_Add-BaseDependenciesIfMissing.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/FindFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/InputFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Minimatch.dll delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/OutFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/ToolFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/TraceFunctions.ps1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/VstsTaskSdk.dll delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 delete mode 100644 bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/lib.json delete mode 100644 bc-tools-extension/Get-BCDependencies/wrapper_Get-BCDependencies.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/FindFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/InputFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Minimatch.dll delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/OutFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/ToolFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/TraceFunctions.ps1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/VstsTaskSdk.dll delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/VstsTaskSdk.psm1 delete mode 100644 bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/lib.json delete mode 100644 bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 delete mode 100644 bc-tools-extension/_common/CommonTools.ps1 diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 deleted file mode 100644 index c5ff2dd..0000000 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.ps1 +++ /dev/null @@ -1,124 +0,0 @@ -<# -.SYNOPSIS - [WINDOWS ONLY] This function actually builds the AL package based on the inputs. -.DESCRIPTION - [WINDOWS ONLY] - This function invokes the AL compiler and compiles the project based on the inputs. This function assumes that the user has already - run the previous two functions 'Get-VSIXCompiler' and 'Get-BCDependencies'. If the inputs are blank, no compilation will occur. It - produces an .app file at the specified location. **It is up to the user to supply a properly versioned application name.** -.PARAMETER EntireAppName - This is the output name of the compiled product, with ".app" appended to it. i.e. "TestProject.1.1.1" will become "TestProject.1.1.1.app" -.PARAMETER BaseProjectDirectory - This is the TOP LEVEL directory of your .al source code. The system will enumerate all folders underneath this one to compile the project -.PARAMETER PackagesDirectory - This is the directory that contains the already-downloaded .app files used in compilation. -.PARAMETER OutputDirectory - This is the destination folder of the .app file. -.PARAMETER ALEXEPath - This is the path to the folder ABOVE the ALC.EXE folder. It will automatically drill into the 'win32' folder in this directory. This - reference is returned by "Get-BCDependencies" in $foo.ALEXEPath. -.OUTPUTS - The path and file name of the compiled application. -.NOTES - SEE DESCRIPTION FOR WINDOWS ONLY REQUIREMENT - Author : James McCullough - Company : Evergrowth Consulting - Version : 1.0.0 - Created : 2025-05-21 - Purpose : DevOps-compatible AL extension compiler invocation -#> -function Build-ALPackage { - [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [String]$EntireAppName, - - [Parameter(Mandatory)] - [String]$BaseProjectDirectory, - - [Parameter()] - [String]$PackagesDirectory, - - [Parameter(Mandatory)] - [String]$OutputDirectory, - - [Parameter(Mandatory)] - [String]$ALEXEPath - ) - - Write-Host "Normalizing: $ALEXEPath" - $ALEXEPath = [System.IO.Path]::GetFullPath($ALEXEPath) - Write-Host "Normalized: $ALEXEPath" - - if ((Test-Path -Path $ALEXEPath) -or ([System.IO.Path]::GetFullPath($ALEXEPath))) { - $checkRef = Split-Path -Path $ALEXEPath -Leaf - if ($checkRef -eq "alc.exe" -or $checkRef -eq "alc") { - Write-Host "Confirmed existence of ALC[.EXE] at $ALEXEPath" - $alcReference = $ALEXEPath - Write-Host "alcReference: $alcReference" - - $expectedEnvPath = if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - Write-Host "Changing execution flag on $alcReference" - icacls $alcReference /grant Everyone:RX | Out-Null - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - Write-Host "Changing execution flag on $alcReference" - icacls $alcReference /grant Everyone:RX | Out-Null - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - Write-Host "Changing execution flag on $alcReference" - chmod +x $alcReference - } - } else { - Write-Error "Not sure what $ALEXEPath has, but the leaf is not (apparenlty) the compiler:" - Write-Error "ALEXEPath: $ALEXEPath" - Write-Error "Leaf: $checkRef" - exit 1 - } - } else { - Write-Error "Having a problem with ALC[.EXE] location. Received '$ALEXEPath' but can't parse where the compiler is. Enumerating file system:" - Get-ChildItem -Path $expandFolder -Recurse | ForEach-Object { Write-Host $_.FullName } - exit 1 - } - - if (-not (Test-Path -Path $PackagesDirectory)) { - throw "Cannot find packages directory: $PackagesDirectory" - } else { - Write-Host "Confirmed packages directory: $PackagesDirectory" - Write-Host "Checking for *.app files" - if (-not (Get-ChildItem -Path $PackagesDirectory -Filter *.app)) { - throw "Found packages directory, but no packages in it" - } - } - - if (-not $EntireAppName.EndsWith(".app")) { - throw "Invalid app name (must end with '.app'): $EntireAppName" - } - - if (-not (Test-Path -Path $BaseProjectDirectory)) { - throw "Cannot find path $BaseProjectDirectory" - } - - if (-not (Test-Path -Path $OutputDirectory)) { - Create-Item -ItemType Directory $OutputDirectory - Write-Host "Created directory: $OutputDirectory" - } else { - Write-Host "Confirmed output directory: $OutputDirectory" - } - - $OutputFile = Join-Path -Path $OutputDirectory -ChildPath $EntireAppName - - & "$alcReference" /project:"$BaseProjectDirectory" /out:"$OutputFile" /packagecachepath:"$PackagesDirectory" - - if ($LASTEXITCODE -ne 0) { - throw "ALC compilation failed with exist code $LASTEXITCODE" - } - - if (-not (Test-Path -Path $OutputFile)) { - throw "Something went wrong; there is no file at $OutputFile" - } else { - Write-Host "Package created at $OutputFile" - } - - return $OutputFile -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/FindFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/FindFunctions.ps1 deleted file mode 100644 index c0278ea..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/FindFunctions.ps1 +++ /dev/null @@ -1,728 +0,0 @@ -<# -.SYNOPSIS -Finds files using match patterns. - -.DESCRIPTION -Determines the find root from a list of patterns. Performs the find and then applies the glob patterns. Supports interleaved exclude patterns. Unrooted patterns are rooted using defaultRoot, unless matchOptions.matchBase is specified and the pattern is a basename only. For matchBase cases, the defaultRoot is used as the find root. - -.PARAMETER DefaultRoot -Default path to root unrooted patterns. Falls back to System.DefaultWorkingDirectory or current location. - -.PARAMETER Pattern -Patterns to apply. Supports interleaved exclude patterns. - -.PARAMETER FindOptions -When the FindOptions parameter is not specified, defaults to (New-VstsFindOptions -FollowSymbolicLinksTrue). Following soft links is generally appropriate unless deleting files. - -.PARAMETER MatchOptions -When the MatchOptions parameter is not specified, defaults to (New-VstsMatchOptions -Dot -NoBrace -NoCase). -#> -function Find-Match { - [CmdletBinding()] - param( - [Parameter()] - [string]$DefaultRoot, - [Parameter()] - [string[]]$Pattern, - $FindOptions, - $MatchOptions) - - Trace-EnteringInvocation $MyInvocation -Parameter None - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Apply defaults for parameters and trace. - if (!$DefaultRoot) { - $DefaultRoot = Get-TaskVariable -Name 'System.DefaultWorkingDirectory' -Default (Get-Location).Path - } - - Write-Verbose "DefaultRoot: '$DefaultRoot'" - if (!$FindOptions) { - $FindOptions = New-FindOptions -FollowSpecifiedSymbolicLink -FollowSymbolicLinks - } - - Trace-FindOptions -Options $FindOptions - if (!$MatchOptions) { - $MatchOptions = New-MatchOptions -Dot -NoBrace -NoCase - } - - Trace-MatchOptions -Options $MatchOptions - Add-Type -LiteralPath $PSScriptRoot\Minimatch.dll - - # Normalize slashes for root dir. - $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot - - $results = @{ } - $originalMatchOptions = $MatchOptions - foreach ($pat in $Pattern) { - Write-Verbose "Pattern: '$pat'" - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue - } - - # Clone match options. - $MatchOptions = Copy-MatchOptions -Options $originalMatchOptions - - # Skip comments. - if (!$MatchOptions.NoComment -and $pat.StartsWith('#')) { - Write-Verbose 'Skipping comment.' - continue - } - - # Set NoComment. Brace expansion could result in a leading '#'. - $MatchOptions.NoComment = $true - - # Determine whether pattern is include or exclude. - $negateCount = 0 - if (!$MatchOptions.NoNegate) { - while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') { - $negateCount++ - } - - $pat = $pat.Substring($negateCount) # trim leading '!' - if ($negateCount) { - Write-Verbose "Trimmed leading '!'. Pattern: '$pat'" - } - } - - $isIncludePattern = $negateCount -eq 0 -or - ($negateCount % 2 -eq 0 -and !$MatchOptions.FlipNegate) -or - ($negateCount % 2 -eq 1 -and $MatchOptions.FlipNegate) - - # Set NoNegate. Brace expansion could result in a leading '!'. - $MatchOptions.NoNegate = $true - $MatchOptions.FlipNegate = $false - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue - } - - # Expand braces - required to accurately interpret findPath. - $expanded = $null - $preExpanded = $pat - if ($MatchOptions.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 $MatchOptions)) - } - - # Set NoBrace. - $MatchOptions.NoBrace = $true - - foreach ($pat in $expanded) { - if ($pat -ne $preExpanded) { - Write-Verbose "Pattern: '$pat'" - } - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose "Skipping empty pattern." - continue - } - - if ($isIncludePattern) { - # Determine the findPath. - $findInfo = Get-FindInfoFromPattern -DefaultRoot $DefaultRoot -Pattern $pat -MatchOptions $MatchOptions - $findPath = $findInfo.FindPath - Write-Verbose "FindPath: '$findPath'" - - if (!$findPath) { - Write-Verbose "Skipping empty path." - continue - } - - # Perform the find. - Write-Verbose "StatOnly: '$($findInfo.StatOnly)'" - [string[]]$findResults = @( ) - if ($findInfo.StatOnly) { - # Simply stat the path - all path segments were used to build the path. - if ((Test-Path -LiteralPath $findPath)) { - $findResults += $findPath - } - } else { - $findResults = @( Get-FindResult -Path $findPath -Options $FindOptions ) - } - - Write-Verbose "Found $($findResults.Count) paths." - - # Apply the pattern. - Write-Verbose "Applying include pattern." - if ($findInfo.AdjustedPattern -ne $pat) { - Write-Verbose "AdjustedPattern: '$($findInfo.AdjustedPattern)'" - $pat = $findInfo.AdjustedPattern - } - - $matchResults = [Minimatch.Minimatcher]::Filter( - $findResults, - $pat, - (ConvertTo-MinimatchOptions -Options $MatchOptions)) - - # Union the results. - $matchCount = 0 - foreach ($matchResult in $matchResults) { - $matchCount++ - $results[$matchResult.ToUpperInvariant()] = $matchResult - } - - Write-Verbose "$matchCount matches" - } else { - # Check if basename only and MatchBase=true. - if ($MatchOptions.MatchBase -and - !(Test-Rooted -Path $pat) -and - ($pat -replace '\\', '/').IndexOf('/') -lt 0) { - - # Do not root the pattern. - Write-Verbose "MatchBase and basename only." - } else { - # Root the exclude pattern. - $pat = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $pat - Write-Verbose "After Get-RootedPattern, pattern: '$pat'" - } - - # Apply the pattern. - Write-Verbose 'Applying exclude pattern.' - $matchResults = [Minimatch.Minimatcher]::Filter( - [string[]]$results.Values, - $pat, - (ConvertTo-MinimatchOptions -Options $MatchOptions)) - - # Subtract the results. - $matchCount = 0 - foreach ($matchResult in $matchResults) { - $matchCount++ - $results.Remove($matchResult.ToUpperInvariant()) - } - - Write-Verbose "$matchCount matches" - } - } - } - - $finalResult = @( $results.Values | Sort-Object ) - Write-Verbose "$($finalResult.Count) final results" - return $finalResult - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Creates FindOptions for use with Find-VstsMatch. - -.DESCRIPTION -Creates FindOptions for use with Find-VstsMatch. Contains switches to control whether to follow symlinks. - -.PARAMETER FollowSpecifiedSymbolicLink -Indicates whether to traverse descendants if the specified path is a symbolic link directory. Does not cause nested symbolic link directories to be traversed. - -.PARAMETER FollowSymbolicLinks -Indicates whether to traverse descendants of symbolic link directories. -#> -function New-FindOptions { - [CmdletBinding()] - param( - [switch]$FollowSpecifiedSymbolicLink, - [switch]$FollowSymbolicLinks) - - return New-Object psobject -Property @{ - FollowSpecifiedSymbolicLink = $FollowSpecifiedSymbolicLink.IsPresent - FollowSymbolicLinks = $FollowSymbolicLinks.IsPresent - } -} - -<# -.SYNOPSIS -Creates MatchOptions for use with Find-VstsMatch and Select-VstsMatch. - -.DESCRIPTION -Creates MatchOptions for use with Find-VstsMatch and Select-VstsMatch. Contains switches to control which pattern matching options are applied. -#> -function New-MatchOptions { - [CmdletBinding()] - param( - [switch]$Dot, - [switch]$FlipNegate, - [switch]$MatchBase, - [switch]$NoBrace, - [switch]$NoCase, - [switch]$NoComment, - [switch]$NoExt, - [switch]$NoGlobStar, - [switch]$NoNegate, - [switch]$NoNull) - - return New-Object psobject -Property @{ - Dot = $Dot.IsPresent - FlipNegate = $FlipNegate.IsPresent - MatchBase = $MatchBase.IsPresent - NoBrace = $NoBrace.IsPresent - NoCase = $NoCase.IsPresent - NoComment = $NoComment.IsPresent - NoExt = $NoExt.IsPresent - NoGlobStar = $NoGlobStar.IsPresent - NoNegate = $NoNegate.IsPresent - NoNull = $NoNull.IsPresent - } -} - -<# -.SYNOPSIS -Applies match patterns against a list of files. - -.DESCRIPTION -Applies match patterns to a list of paths. Supports interleaved exclude patterns. - -.PARAMETER ItemPath -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). -#> -function Select-Match { - [CmdletBinding()] - param( - [Parameter()] - [string[]]$ItemPath, - [Parameter()] - [string[]]$Pattern, - [Parameter()] - [string]$PatternRoot, - $Options) - - - Trace-EnteringInvocation $MyInvocation -Parameter None - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - if (!$Options) { - $Options = New-MatchOptions -Dot -NoBrace -NoCase - } - - Trace-MatchOptions -Options $Options - Add-Type -LiteralPath $PSScriptRoot\Minimatch.dll - - # Hashtable to keep track of matches. - $map = @{ } - - $originalOptions = $Options - foreach ($pat in $Pattern) { - Write-Verbose "Pattern: '$pat'" - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue - } - - # Clone match options. - $Options = Copy-MatchOptions -Options $originalOptions - - # Skip comments. - if (!$Options.NoComment -and $pat.StartsWith('#')) { - Write-Verbose 'Skipping comment.' - continue - } - - # Set NoComment. Brace expansion could result in a leading '#'. - $Options.NoComment = $true - - # Determine whether pattern is include or exclude. - $negateCount = 0 - if (!$Options.NoNegate) { - while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') { - $negateCount++ - } - - $pat = $pat.Substring($negateCount) # trim leading '!' - if ($negateCount) { - Write-Verbose "Trimmed leading '!'. Pattern: '$pat'" - } - } - - $isIncludePattern = $negateCount -eq 0 -or - ($negateCount % 2 -eq 0 -and !$Options.FlipNegate) -or - ($negateCount % 2 -eq 1 -and $Options.FlipNegate) - - # Set NoNegate. Brace expansion could result in a leading '!'. - $Options.NoNegate = $true - $Options.FlipNegate = $false - - # 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)) - } - - # Set NoBrace. - $Options.NoBrace = $true - - foreach ($pat in $expanded) { - if ($pat -ne $preExpanded) { - Write-Verbose "Pattern: '$pat'" - } - - # 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)) { - - # Root the include pattern. - $pat = Get-RootedPattern -DefaultRoot $PatternRoot -Pattern $pat - Write-Verbose "After Get-RootedPattern, pattern: '$pat'" - } - - 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" - } - } - } - - # return a filtered version of the original list (preserves order and prevents duplication) - $result = $ItemPath | Where-Object { $map[$_] } - Write-Verbose "$($result.Count) final results" - $result - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -################################################################################ -# Private functions. -################################################################################ - -function Copy-MatchOptions { - [CmdletBinding()] - param($Options) - - return New-Object psobject -Property @{ - Dot = $Options.Dot -eq $true - FlipNegate = $Options.FlipNegate -eq $true - MatchBase = $Options.MatchBase -eq $true - NoBrace = $Options.NoBrace -eq $true - NoCase = $Options.NoCase -eq $true - NoComment = $Options.NoComment -eq $true - NoExt = $Options.NoExt -eq $true - NoGlobStar = $Options.NoGlobStar -eq $true - NoNegate = $Options.NoNegate -eq $true - NoNull = $Options.NoNull -eq $true - } -} - -function ConvertTo-MinimatchOptions { - [CmdletBinding()] - param($Options) - - $opt = New-Object Minimatch.Options - $opt.AllowWindowsPaths = $true - $opt.Dot = $Options.Dot -eq $true - $opt.FlipNegate = $Options.FlipNegate -eq $true - $opt.MatchBase = $Options.MatchBase -eq $true - $opt.NoBrace = $Options.NoBrace -eq $true - $opt.NoCase = $Options.NoCase -eq $true - $opt.NoComment = $Options.NoComment -eq $true - $opt.NoExt = $Options.NoExt -eq $true - $opt.NoGlobStar = $Options.NoGlobStar -eq $true - $opt.NoNegate = $Options.NoNegate -eq $true - $opt.NoNull = $Options.NoNull -eq $true - return $opt -} - -function ConvertTo-NormalizedSeparators { - [CmdletBinding()] - param([string]$Path) - - # Convert slashes. - $Path = "$Path".Replace('/', '\') - - # Remove redundant slashes. - $isUnc = $Path -match '^\\\\+[^\\]' - $Path = $Path -replace '\\\\+', '\' - if ($isUnc) { - $Path = '\' + $Path - } - - return $Path -} - -function Get-FindInfoFromPattern { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$DefaultRoot, - [Parameter(Mandatory = $true)] - [string]$Pattern, - [Parameter(Mandatory = $true)] - $MatchOptions) - - if (!$MatchOptions.NoBrace) { - throw "Get-FindInfoFromPattern expected MatchOptions.NoBrace to be true." - } - - # For the sake of determining the find path, pretend NoCase=false. - $MatchOptions = Copy-MatchOptions -Options $MatchOptions - $MatchOptions.NoCase = $false - - # Check if basename only and MatchBase=true - if ($MatchOptions.MatchBase -and - !(Test-Rooted -Path $Pattern) -and - ($Pattern -replace '\\', '/').IndexOf('/') -lt 0) { - - return New-Object psobject -Property @{ - AdjustedPattern = $Pattern - FindPath = $DefaultRoot - StatOnly = $false - } - } - - # The technique applied by this function is to use the information on the Minimatch object determine - # the findPath. Minimatch breaks the pattern into path segments, and exposes information about which - # segments are literal vs patterns. - # - # Note, the technique currently imposes a limitation for drive-relative paths with a glob in the - # first segment, e.g. C:hello*/world. It's feasible to overcome this limitation, but is left unsolved - # for now. - $minimatchObj = New-Object Minimatch.Minimatcher($Pattern, (ConvertTo-MinimatchOptions -Options $MatchOptions)) - - # The "set" field is a two-dimensional enumerable of parsed path segment info. The outer enumerable should only - # contain one item, otherwise something went wrong. Brace expansion can result in multiple items in the outer - # enumerable, but that should be turned off by the time this function is reached. - # - # Note, "set" is a private field in the .NET implementation but is documented as a feature in the nodejs - # implementation. The .NET implementation is a port and is by a different author. - $setFieldInfo = $minimatchObj.GetType().GetField('set', 'Instance,NonPublic') - [object[]]$set = $setFieldInfo.GetValue($minimatchObj) - if ($set.Count -ne 1) { - throw "Get-FindInfoFromPattern expected Minimatch.Minimatcher(...).set.Count to be 1. Actual: '$($set.Count)'" - } - - [string[]]$literalSegments = @( ) - [object[]]$parsedSegments = $set[0] - foreach ($parsedSegment in $parsedSegments) { - if ($parsedSegment.GetType().Name -eq 'LiteralItem') { - # The item is a LiteralItem when the original input for the path segment does not contain any - # unescaped glob characters. - $literalSegments += $parsedSegment.Source; - continue - } - - break; - } - - # Join the literal segments back together. Minimatch converts '\' to '/' on Windows, then squashes - # consequetive slashes, and finally splits on slash. This means that UNC format is lost, but can - # be detected from the original pattern. - $joinedSegments = [string]::Join('/', $literalSegments) - if ($joinedSegments -and ($Pattern -replace '\\', '/').StartsWith('//')) { - $joinedSegments = '/' + $joinedSegments # restore UNC format - } - - # Determine the find path. - $findPath = '' - if ((Test-Rooted -Path $Pattern)) { # The pattern is rooted. - $findPath = $joinedSegments - } elseif ($joinedSegments) { # The pattern is not rooted, and literal segements were found. - $findPath = [System.IO.Path]::Combine($DefaultRoot, $joinedSegments) - } else { # The pattern is not rooted, and no literal segements were found. - $findPath = $DefaultRoot - } - - # Clean up the path. - if ($findPath) { - $findPath = [System.IO.Path]::GetDirectoryName(([System.IO.Path]::Combine($findPath, '_'))) # Hack to remove unnecessary trailing slash. - $findPath = ConvertTo-NormalizedSeparators -Path $findPath - } - - return New-Object psobject -Property @{ - AdjustedPattern = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $Pattern - FindPath = $findPath - StatOnly = $literalSegments.Count -eq $parsedSegments.Count - } -} - -function Get-FindResult { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - $Options) - - if (!(Test-Path -LiteralPath $Path)) { - Write-Verbose 'Path not found.' - return - } - - $Path = ConvertTo-NormalizedSeparators -Path $Path - - # Push the first item. - [System.Collections.Stack]$stack = New-Object System.Collections.Stack - $stack.Push((Get-Item -LiteralPath $Path)) - - $count = 0 - while ($stack.Count) { - # Pop the next item and yield the result. - $item = $stack.Pop() - $count++ - $item.FullName - - # Traverse. - if (($item.Attributes -band 0x00000010) -eq 0x00000010) { # Directory - if (($item.Attributes -band 0x00000400) -ne 0x00000400 -or # ReparsePoint - $Options.FollowSymbolicLinks -or - ($count -eq 1 -and $Options.FollowSpecifiedSymbolicLink)) { - - $childItems = @( Get-DirectoryChildItem -Path $Item.FullName -Force ) - [System.Array]::Reverse($childItems) - foreach ($childItem in $childItems) { - $stack.Push($childItem) - } - } - } - } -} - -function Get-RootedPattern { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$DefaultRoot, - [Parameter(Mandatory = $true)] - [string]$Pattern) - - if ((Test-Rooted -Path $Pattern)) { - return $Pattern - } - - # Normalize root. - $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot - - # Escape special glob characters. - $DefaultRoot = $DefaultRoot -replace '(\[)(?=[^\/]+\])', '[[]' # Escape '[' when ']' follows within the path segment - $DefaultRoot = $DefaultRoot.Replace('?', '[?]') # Escape '?' - $DefaultRoot = $DefaultRoot.Replace('*', '[*]') # Escape '*' - $DefaultRoot = $DefaultRoot -replace '\+\(', '[+](' # Escape '+(' - $DefaultRoot = $DefaultRoot -replace '@\(', '[@](' # Escape '@(' - $DefaultRoot = $DefaultRoot -replace '!\(', '[!](' # Escape '!(' - - if ($DefaultRoot -like '[A-Z]:') { # e.g. C: - return "$DefaultRoot$Pattern" - } - - # Ensure root ends with a separator. - if (!$DefaultRoot.EndsWith('\')) { - $DefaultRoot = "$DefaultRoot\" - } - - return "$DefaultRoot$Pattern" -} - -function Test-Rooted { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path) - - $Path = ConvertTo-NormalizedSeparators -Path $Path - return $Path.StartsWith('\') -or # e.g. \ or \hello or \\hello - $Path -like '[A-Z]:*' # e.g. C: or C:\hello -} - -function Trace-MatchOptions { - [CmdletBinding()] - param($Options) - - Write-Verbose "MatchOptions.Dot: '$($Options.Dot)'" - Write-Verbose "MatchOptions.FlipNegate: '$($Options.FlipNegate)'" - Write-Verbose "MatchOptions.MatchBase: '$($Options.MatchBase)'" - Write-Verbose "MatchOptions.NoBrace: '$($Options.NoBrace)'" - Write-Verbose "MatchOptions.NoCase: '$($Options.NoCase)'" - Write-Verbose "MatchOptions.NoComment: '$($Options.NoComment)'" - Write-Verbose "MatchOptions.NoExt: '$($Options.NoExt)'" - Write-Verbose "MatchOptions.NoGlobStar: '$($Options.NoGlobStar)'" - Write-Verbose "MatchOptions.NoNegate: '$($Options.NoNegate)'" - Write-Verbose "MatchOptions.NoNull: '$($Options.NoNull)'" -} - -function Trace-FindOptions { - [CmdletBinding()] - param($Options) - - Write-Verbose "FindOptions.FollowSpecifiedSymbolicLink: '$($FindOptions.FollowSpecifiedSymbolicLink)'" - Write-Verbose "FindOptions.FollowSymbolicLinks: '$($FindOptions.FollowSymbolicLinks)'" -} diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/InputFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/InputFunctions.ps1 deleted file mode 100644 index 071d5ca..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/InputFunctions.ps1 +++ /dev/null @@ -1,524 +0,0 @@ -# Hash table of known variable info. The formatted env var name is the lookup key. -# -# The purpose of this hash table is to keep track of known variables. The hash table -# needs to be maintained for multiple reasons: -# 1) to distinguish between env vars and job vars -# 2) to distinguish between secret vars and public -# 3) to know the real variable name and not just the formatted env var name. -$script:knownVariables = @{ } -$script:vault = @{ } - -<# -.SYNOPSIS -Gets an endpoint. - -.DESCRIPTION -Gets an endpoint object for the specified endpoint name. The endpoint is returned as an object with three properties: Auth, Data, and Url. - -The Data property requires a 1.97 agent or higher. - -.PARAMETER Require -Writes an error to the error pipeline if the endpoint is not found. -#> -function Get-Endpoint { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [switch]$Require) - - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Get the URL. - $description = Get-LocString -Key PSLIB_EndpointUrl0 -ArgumentList $Name - $key = "ENDPOINT_URL_$Name" - $url = Get-VaultValue -Description $description -Key $key -Require:$Require - - # Get the auth object. - $description = Get-LocString -Key PSLIB_EndpointAuth0 -ArgumentList $Name - $key = "ENDPOINT_AUTH_$Name" - if ($auth = (Get-VaultValue -Description $description -Key $key -Require:$Require)) { - $auth = ConvertFrom-Json -InputObject $auth - } - - # Get the data. - $description = "'$Name' service endpoint data" - $key = "ENDPOINT_DATA_$Name" - if ($data = (Get-VaultValue -Description $description -Key $key)) { - $data = ConvertFrom-Json -InputObject $data - } - - # Return the endpoint. - if ($url -or $auth -or $data) { - New-Object -TypeName psobject -Property @{ - Url = $url - Auth = $auth - Data = $data - } - } - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } -} - -<# -.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. - -.DESCRIPTION -Gets the value for the specified input name. - -.PARAMETER AsBool -Returns the value as a bool. Returns true if the value converted to a string is "1" or "true" (case insensitive); otherwise false. - -.PARAMETER AsInt -Returns the value as an int. Returns the value converted to an int. Returns 0 if the conversion fails. - -.PARAMETER Default -Default value to use if the input is null or empty. - -.PARAMETER Require -Writes an error to the error pipeline if the input is null or empty. -#> -function Get-Input { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(ParameterSetName = 'Default')] - $Default, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [switch]$AsBool, - [switch]$AsInt) - - # Get the input from the vault. Splat the bound parameters hashtable. Splatting is required - # in order to concisely invoke the correct parameter set. - $null = $PSBoundParameters.Remove('Name') - $description = Get-LocString -Key PSLIB_Input0 -ArgumentList $Name - $key = "INPUT_$($Name.Replace(' ', '_').ToUpperInvariant())" - Get-VaultValue @PSBoundParameters -Description $description -Key $key -} - -<# -.SYNOPSIS -Gets a task variable. - -.DESCRIPTION -Gets the value for the specified task variable. - -.PARAMETER AsBool -Returns the value as a bool. Returns true if the value converted to a string is "1" or "true" (case insensitive); otherwise false. - -.PARAMETER AsInt -Returns the value as an int. Returns the value converted to an int. Returns 0 if the conversion fails. - -.PARAMETER Default -Default value to use if the input is null or empty. - -.PARAMETER Require -Writes an error to the error pipeline if the input is null or empty. -#> -function Get-TaskVariable { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(ParameterSetName = 'Default')] - $Default, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [switch]$AsBool, - [switch]$AsInt) - - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - $description = Get-LocString -Key PSLIB_TaskVariable0 -ArgumentList $Name - $variableKey = Get-VariableKey -Name $Name - if ($script:knownVariables.$variableKey.Secret) { - # Get secret variable. Splatting is required to concisely invoke the correct parameter set. - $null = $PSBoundParameters.Remove('Name') - $vaultKey = "SECRET_$variableKey" - Get-VaultValue @PSBoundParameters -Description $description -Key $vaultKey - } else { - # Get public variable. - $item = $null - $path = "Env:$variableKey" - if ((Test-Path -LiteralPath $path) -and ($item = Get-Item -LiteralPath $path).Value) { - # Intentionally empty. Value was successfully retrieved. - } elseif (!$script:nonInteractive) { - # The value wasn't found and the module is running in interactive dev mode. - # Prompt for the value. - Set-Item -LiteralPath $path -Value (Read-Host -Prompt $description) - if (Test-Path -LiteralPath $path) { - $item = Get-Item -LiteralPath $path - } - } - - # Get the converted value. Splatting is required to concisely invoke the correct parameter set. - $null = $PSBoundParameters.Remove('Name') - Get-Value @PSBoundParameters -Description $description -Key $variableKey -Value $item.Value - } - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } -} - -<# -.SYNOPSIS -Gets all job variables available to the task. Requires 2.104.1 agent or higher. - -.DESCRIPTION -Gets a snapshot of the current state of all job variables available to the task. -Requires a 2.104.1 agent or higher for full functionality. - -Returns an array of objects with the following properties: - [string]Name - [string]Value - [bool]Secret - -Limitations on an agent prior to 2.104.1: - 1) The return value does not include all public variables. Only public variables - that have been added using setVariable are returned. - 2) The name returned for each secret variable is the formatted environment variable - name, not the actual variable name (unless it was set explicitly at runtime using - setVariable). -#> -function Get-TaskVariableInfo { - [CmdletBinding()] - param() - - foreach ($info in $script:knownVariables.Values) { - New-Object -TypeName psobject -Property @{ - Name = $info.Name - Value = Get-TaskVariable -Name $info.Name - Secret = $info.Secret - } - } -} - -<# -.SYNOPSIS -Sets a task variable. - -.DESCRIPTION -Sets a task variable in the current task context as well as in the current job context. This allows the task variable to retrieved by subsequent tasks within the same job. -#> -function Set-TaskVariable { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [string]$Value, - [switch]$Secret) - - # Once a secret always a secret. - $variableKey = Get-VariableKey -Name $Name - [bool]$Secret = $Secret -or $script:knownVariables.$variableKey.Secret - if ($Secret) { - $vaultKey = "SECRET_$variableKey" - if (!$Value) { - # Clear the secret. - Write-Verbose "Set $Name = ''" - $script:vault.Remove($vaultKey) - } else { - # Store the secret in the vault. - Write-Verbose "Set $Name = '********'" - $script:vault[$vaultKey] = New-Object System.Management.Automation.PSCredential( - $vaultKey, - (ConvertTo-SecureString -String $Value -AsPlainText -Force)) - } - - # Clear the environment variable. - Set-Item -LiteralPath "Env:$variableKey" -Value '' - } else { - # Set the environment variable. - Write-Verbose "Set $Name = '$Value'" - Set-Item -LiteralPath "Env:$variableKey" -Value $Value - } - - # Store the metadata. - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - Name = $name - Secret = $Secret - } - - # Persist the variable in the task context. - Write-SetVariable -Name $Name -Value $Value -Secret:$Secret -} - -<# -.SYNOPSIS -Gets the value of an task feature and converts to a bool. - -.PARAMETER $FeatureName -Name of the feature to get. - -.NOTES -This method is only for internal Microsoft development. Do not use it for external tasks. -#> -function Get-PipelineFeature { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$FeatureName - ) - - $featureValue = Get-TaskVariable -Name "DistributedTask.Tasks.$FeatureName" - - if (!$featureValue) { - Write-Debug "Feature '$FeatureName' is not set. Defaulting to 'false'" - return $false - } - - $boolValue = $featureValue.ToLowerInvariant() -eq 'true' - - Write-Debug "Feature '$FeatureName' = '$featureValue'. Processed as '$boolValue'" - - return $boolValue -} - -######################################## -# Private functions. -######################################## -function Get-VaultValue { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$Description, - [Parameter(Mandatory = $true)] - [string]$Key, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [Parameter(ParameterSetName = 'Default')] - [object]$Default, - [switch]$AsBool, - [switch]$AsInt) - - # Attempt to get the vault value. - $value = $null - if ($psCredential = $script:vault[$Key]) { - $value = $psCredential.GetNetworkCredential().Password - } elseif (!$script:nonInteractive) { - # The value wasn't found. Prompt for the value if running in interactive dev mode. - $value = Read-Host -Prompt $Description - if ($value) { - $script:vault[$Key] = New-Object System.Management.Automation.PSCredential( - $Key, - (ConvertTo-SecureString -String $value -AsPlainText -Force)) - } - } - - Get-Value -Value $value @PSBoundParameters -} - -function Get-Value { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [string]$Value, - [Parameter(Mandatory = $true)] - [string]$Description, - [Parameter(Mandatory = $true)] - [string]$Key, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [Parameter(ParameterSetName = 'Default')] - [object]$Default, - [switch]$AsBool, - [switch]$AsInt) - - $result = $Value - if ($result) { - if ($Key -like 'ENDPOINT_AUTH_*') { - Write-Verbose "$($Key): '********'" - } else { - Write-Verbose "$($Key): '$result'" - } - } else { - Write-Verbose "$Key (empty)" - - # Write error if required. - if ($Require) { - Write-Error "$(Get-LocString -Key PSLIB_Required0 $Description)" - return - } - - # Fallback to the default if provided. - if ($PSCmdlet.ParameterSetName -eq 'Default') { - $result = $Default - $OFS = ' ' - Write-Verbose " Defaulted to: '$result'" - } else { - $result = '' - } - } - - # Convert to bool if specified. - if ($AsBool) { - if ($result -isnot [bool]) { - $result = "$result" -in '1', 'true' - Write-Verbose " Converted to bool: $result" - } - - return $result - } - - # Convert to int if specified. - if ($AsInt) { - if ($result -isnot [int]) { - try { - $result = [int]"$result" - } catch { - $result = 0 - } - - Write-Verbose " Converted to int: $result" - } - - return $result - } - - return $result -} - -function Initialize-Inputs { - # Store endpoints, inputs, and secret variables in the vault. - foreach ($variable in (Get-ChildItem -Path Env:ENDPOINT_?*, Env:INPUT_?*, Env:SECRET_?*, Env:SECUREFILE_?*)) { - # Record the secret variable metadata. This is required by Get-TaskVariable to - # retrieve the value. In a 2.104.1 agent or higher, this metadata will be overwritten - # when $env:VSTS_SECRET_VARIABLES is processed. - if ($variable.Name -like 'SECRET_?*') { - $variableKey = $variable.Name.Substring('SECRET_'.Length) - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - # This is technically not the variable name (has underscores instead of dots), - # but it's good enough to make Get-TaskVariable work in a pre-2.104.1 agent - # where $env:VSTS_SECRET_VARIABLES is not defined. - Name = $variableKey - Secret = $true - } - } - - # Store the value in the vault. - $vaultKey = $variable.Name - if ($variable.Value) { - $script:vault[$vaultKey] = New-Object System.Management.Automation.PSCredential( - $vaultKey, - (ConvertTo-SecureString -String $variable.Value -AsPlainText -Force)) - } - - # Clear the environment variable. - Remove-Item -LiteralPath "Env:$($variable.Name)" - } - - # Record the public variable names. Env var added in 2.104.1 agent. - if ($env:VSTS_PUBLIC_VARIABLES) { - foreach ($name in (ConvertFrom-Json -InputObject $env:VSTS_PUBLIC_VARIABLES)) { - $variableKey = Get-VariableKey -Name $name - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - Name = $name - Secret = $false - } - } - - $env:VSTS_PUBLIC_VARIABLES = '' - } - - # Record the secret variable names. Env var added in 2.104.1 agent. - if ($env:VSTS_SECRET_VARIABLES) { - foreach ($name in (ConvertFrom-Json -InputObject $env:VSTS_SECRET_VARIABLES)) { - $variableKey = Get-VariableKey -Name $name - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - Name = $name - Secret = $true - } - } - - $env:VSTS_SECRET_VARIABLES = '' - } -} - -function Get-VariableKey { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name) - - if ($Name -ne 'agent.jobstatus') { - $Name = $Name.Replace('.', '_') - } - - $Name.ToUpperInvariant() -} diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 deleted file mode 100644 index 9e9e9ec..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 +++ /dev/null @@ -1,320 +0,0 @@ -<# -.SYNOPSIS -Finds files or directories. - -.DESCRIPTION -Finds files or directories using advanced pattern matching. - -.PARAMETER LiteralDirectory -Directory to search. - -.PARAMETER LegacyPattern -Proprietary pattern format. The LiteralDirectory parameter is used to root any unrooted patterns. - -Separate multiple patterns using ";". Escape actual ";" in the path by using ";;". -"?" indicates a wildcard that represents any single character within a path segment. -"*" indicates a wildcard that represents zero or more characters within a path segment. -"**" as the entire path segment indicates a recursive search. -"**" within a path segment indicates a recursive intersegment wildcard. -"+:" (can be omitted) indicates an include pattern. -"-:" indicates an exclude pattern. - -The result is from the command is a union of all the matches from the include patterns, minus the matches from the exclude patterns. - -.PARAMETER IncludeFiles -Indicates whether to include files in the results. - -If neither IncludeFiles or IncludeDirectories is set, then IncludeFiles is assumed. - -.PARAMETER IncludeDirectories -Indicates whether to include directories in the results. - -If neither IncludeFiles or IncludeDirectories is set, then IncludeFiles is assumed. - -.PARAMETER Force -Indicates whether to include hidden items. - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\Is?Match.txt" - -Given: -C:\Directory\Is1Match.txt -C:\Directory\Is2Match.txt -C:\Directory\IsNotMatch.txt - -Returns: -C:\Directory\Is1Match.txt -C:\Directory\Is2Match.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\Is*Match.txt" - -Given: -C:\Directory\IsOneMatch.txt -C:\Directory\IsTwoMatch.txt -C:\Directory\NonMatch.txt - -Returns: -C:\Directory\IsOneMatch.txt -C:\Directory\IsTwoMatch.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\**\Match.txt" - -Given: -C:\Directory\Match.txt -C:\Directory\NotAMatch.txt -C:\Directory\SubDir\Match.txt -C:\Directory\SubDir\SubSubDir\Match.txt - -Returns: -C:\Directory\Match.txt -C:\Directory\SubDir\Match.txt -C:\Directory\SubDir\SubSubDir\Match.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\**" - -Given: -C:\Directory\One.txt -C:\Directory\SubDir\Two.txt -C:\Directory\SubDir\SubSubDir\Three.txt - -Returns: -C:\Directory\One.txt -C:\Directory\SubDir\Two.txt -C:\Directory\SubDir\SubSubDir\Three.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\Sub**Match.txt" - -Given: -C:\Directory\IsNotAMatch.txt -C:\Directory\SubDir\IsAMatch.txt -C:\Directory\SubDir\IsNot.txt -C:\Directory\SubDir\SubSubDir\IsAMatch.txt -C:\Directory\SubDir\SubSubDir\IsNot.txt - -Returns: -C:\Directory\SubDir\IsAMatch.txt -C:\Directory\SubDir\SubSubDir\IsAMatch.txt -#> -function Find-Files { - [CmdletBinding()] - param( - [ValidateNotNullOrEmpty()] - [Parameter()] - [string]$LiteralDirectory, - [Parameter(Mandatory = $true)] - [string]$LegacyPattern, - [switch]$IncludeFiles, - [switch]$IncludeDirectories, - [switch]$Force) - - # Note, due to subtle implementation details of Get-PathPrefix/Get-PathIterator, - # this function does not appear to be able to search the root of a drive and other - # cases where Path.GetDirectoryName() returns empty. More details in Get-PathPrefix. - - Trace-EnteringInvocation $MyInvocation - if (!$IncludeFiles -and !$IncludeDirectories) { - $IncludeFiles = $true - } - - $includePatterns = New-Object System.Collections.Generic.List[string] - $excludePatterns = New-Object System.Collections.Generic.List[System.Text.RegularExpressions.Regex] - $LegacyPattern = $LegacyPattern.Replace(';;', "`0") - foreach ($pattern in $LegacyPattern.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)) { - $pattern = $pattern.Replace("`0", ';') - $isIncludePattern = Test-IsIncludePattern -Pattern ([ref]$pattern) - if ($LiteralDirectory -and !([System.IO.Path]::IsPathRooted($pattern))) { - # Use the root directory provided to make the pattern a rooted path. - $pattern = [System.IO.Path]::Combine($LiteralDirectory, $pattern) - } - - # Validate pattern does not end with a \. - if ($pattern.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { - throw (Get-LocString -Key PSLIB_InvalidPattern0 -ArgumentList $pattern) - } - - if ($isIncludePattern) { - $includePatterns.Add($pattern) - } else { - $excludePatterns.Add((Convert-PatternToRegex -Pattern $pattern)) - } - } - - $count = 0 - foreach ($path in (Get-MatchingItems -IncludePatterns $includePatterns -ExcludePatterns $excludePatterns -IncludeFiles:$IncludeFiles -IncludeDirectories:$IncludeDirectories -Force:$Force)) { - $count++ - $path - } - - Write-Verbose "Total found: $count" - Trace-LeavingInvocation $MyInvocation -} - -######################################## -# Private functions. -######################################## -function Convert-PatternToRegex { - [CmdletBinding()] - param([string]$Pattern) - - $Pattern = [regex]::Escape($Pattern.Replace('\', '/')). # Normalize separators and regex escape. - Replace('/\*\*/', '((/.+/)|(/))'). # Replace directory globstar. - Replace('\*\*', '.*'). # Replace remaining globstars with a wildcard that can span directory separators. - Replace('\*', '[^/]*'). # Replace asterisks with a wildcard that cannot span directory separators. - # bug: should be '[^/]' instead of '.' - Replace('\?', '.') # Replace single character wildcards. - New-Object regex -ArgumentList "^$Pattern`$", ([System.Text.RegularExpressions.RegexOptions]::IgnoreCase) -} - -function Get-FileNameFilter { - [CmdletBinding()] - param([string]$Pattern) - - $index = $Pattern.LastIndexOf('\') - if ($index -eq -1 -or # Pattern does not contain a backslash. - !($Pattern = $Pattern.Substring($index + 1)) -or # Pattern ends in a backslash. - $Pattern.Contains('**')) # Last segment contains an inter-segment wildcard. - { - return '*' - } - - # bug? is this supposed to do substring? - return $Pattern -} - -function Get-MatchingItems { - [CmdletBinding()] - param( - [System.Collections.Generic.List[string]]$IncludePatterns, - [System.Collections.Generic.List[regex]]$ExcludePatterns, - [switch]$IncludeFiles, - [switch]$IncludeDirectories, - [switch]$Force) - - Trace-EnteringInvocation $MyInvocation - $allFiles = New-Object System.Collections.Generic.HashSet[string] - foreach ($pattern in $IncludePatterns) { - $pathPrefix = Get-PathPrefix -Pattern $pattern - $fileNameFilter = Get-FileNameFilter -Pattern $pattern - $patternRegex = Convert-PatternToRegex -Pattern $pattern - # Iterate over the directories and files under the pathPrefix. - Get-PathIterator -Path $pathPrefix -Filter $fileNameFilter -IncludeFiles:$IncludeFiles -IncludeDirectories:$IncludeDirectories -Force:$Force | - ForEach-Object { - # Normalize separators. - $normalizedPath = $_.Replace('\', '/') - # **/times/** will not match C:/fun/times because there isn't a trailing slash. - # So try both if including directories. - $alternatePath = "$normalizedPath/" # potential bug: it looks like this will result in a false - # positive if the item is a regular file and not a directory - - $isMatch = $false - if ($patternRegex.IsMatch($normalizedPath) -or ($IncludeDirectories -and $patternRegex.IsMatch($alternatePath))) { - $isMatch = $true - - # Test whether the path should be excluded. - foreach ($regex in $ExcludePatterns) { - if ($regex.IsMatch($normalizedPath) -or ($IncludeDirectories -and $regex.IsMatch($alternatePath))) { - $isMatch = $false - break - } - } - } - - if ($isMatch) { - $null = $allFiles.Add($_) - } - } - } - - Trace-Path -Path $allFiles -PassThru - Trace-LeavingInvocation $MyInvocation -} - -function Get-PathIterator { - [CmdletBinding()] - param( - [string]$Path, - [string]$Filter, - [switch]$IncludeFiles, - [switch]$IncludeDirectories, - [switch]$Force) - - if (!$Path) { - return - } - - # bug: this returns the dir without verifying whether exists - if ($IncludeDirectories) { - $Path - } - - Get-DirectoryChildItem -Path $Path -Filter $Filter -Force:$Force -Recurse | - ForEach-Object { - if ($_.Attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { - if ($IncludeDirectories) { - $_.FullName - } - } elseif ($IncludeFiles) { - $_.FullName - } - } -} - -function Get-PathPrefix { - [CmdletBinding()] - param([string]$Pattern) - - # Note, unable to search root directories is a limitation due to subtleties of this function - # and downstream code in Get-PathIterator that short-circuits when the path prefix is empty. - # This function uses Path.GetDirectoryName() to determine the path prefix, which will yield - # empty in some cases. See the following examples of Path.GetDirectoryName() input => output: - # C:/ => - # C:/hello => C:\ - # C:/hello/ => C:\hello - # C:/hello/world => C:\hello - # C:/hello/world/ => C:\hello\world - # C: => - # C:hello => C: - # C:hello/ => C:hello - # / => - # /hello => \ - # /hello/ => \hello - # //hello => - # //hello/ => - # //hello/world => - # //hello/world/ => \\hello\world - - $index = $Pattern.IndexOfAny([char[]]@('*'[0], '?'[0])) - if ($index -eq -1) { - # If no wildcards are found, return the directory name portion of the path. - # If there is no directory name (file name only in pattern), this will return empty string. - return [System.IO.Path]::GetDirectoryName($Pattern) - } - - [System.IO.Path]::GetDirectoryName($Pattern.Substring(0, $index)) -} - -function Test-IsIncludePattern { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [ref]$Pattern) - - # Include patterns start with +: or anything except -: - # Exclude patterns start with -: - if ($Pattern.value.StartsWith("+:")) { - # Remove the prefix. - $Pattern.value = $Pattern.value.Substring(2) - $true - } elseif ($Pattern.value.StartsWith("-:")) { - # Remove the prefix. - $Pattern.value = $Pattern.value.Substring(2) - $false - } else { - # No prefix, so leave the string alone. - $true; - } -} diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 deleted file mode 100644 index b554970..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 +++ /dev/null @@ -1,150 +0,0 @@ -$script:resourceStrings = @{ } - -<# -.SYNOPSIS -Gets a localized resource string. - -.DESCRIPTION -Gets a localized resource string and optionally formats the string with arguments. - -If the format fails (due to a bad format string or incorrect expected arguments in the format string), then the format string is returned followed by each of the arguments (delimited by a space). - -If the lookup key is not found, then the lookup key is returned followed by each of the arguments (delimited by a space). - -.PARAMETER Require -Writes an error to the error pipeline if the endpoint is not found. -#> -function Get-LocString { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true, Position = 1)] - [string]$Key, - [Parameter(Position = 2)] - [object[]]$ArgumentList = @( )) - - # Due to the dynamically typed nature of PowerShell, a single null argument passed - # to an array parameter is interpreted as a null array. - if ([object]::ReferenceEquals($null, $ArgumentList)) { - $ArgumentList = @( $null ) - } - - # Lookup the format string. - $format = '' - if (!($format = $script:resourceStrings[$Key])) { - # Warn the key was not found. Prevent recursion if the lookup key is the - # "string resource key not found" lookup key. - $resourceNotFoundKey = 'PSLIB_StringResourceKeyNotFound0' - if ($key -ne $resourceNotFoundKey) { - Write-Warning (Get-LocString -Key $resourceNotFoundKey -ArgumentList $Key) - } - - # Fallback to just the key itself if there aren't any arguments to format. - if (!$ArgumentList.Count) { return $key } - - # Otherwise fallback to the key followed by the arguments. - $OFS = " " - return "$key $ArgumentList" - } - - # Return the string if there aren't any arguments to format. - if (!$ArgumentList.Count) { return $format } - - try { - [string]::Format($format, $ArgumentList) - } catch { - Write-Warning (Get-LocString -Key 'PSLIB_StringFormatFailed') - $OFS = " " - "$format $ArgumentList" - } -} - -<# -.SYNOPSIS -Imports resource strings for use with GetVstsLocString. - -.DESCRIPTION -Imports resource strings for use with GetVstsLocString. The imported strings are stored in an internal resource string dictionary. Optionally, if a separate resource file for the current culture exists, then the localized strings from that file then imported (overlaid) into the same internal resource string dictionary. - -Resource strings from the SDK are prefixed with "PSLIB_". This prefix should be avoided for custom resource strings. - -.Parameter LiteralPath -JSON file containing resource strings. - -.EXAMPLE -Import-VstsLocStrings -LiteralPath $PSScriptRoot\Task.json - -Imports strings from messages section in the JSON file. If a messages section is not defined, then no strings are imported. Example messages section: -{ - "messages": { - "Hello": "Hello you!", - "Hello0": "Hello {0}!" - } -} - -.EXAMPLE -Import-VstsLocStrings -LiteralPath $PSScriptRoot\Task.json - -Overlays strings from an optional separate resource file for the current culture. - -Given the task variable System.Culture is set to 'de-DE'. This variable is set by the agent based on the current culture for the job. -Given the file Task.json contains: -{ - "messages": { - "GoodDay": "Good day!", - } -} -Given the file resources.resjson\de-DE\resources.resjson: -{ - "loc.messages.GoodDay": "Guten Tag!" -} - -The net result from the import command would be one new key-value pair added to the internal dictionary: Key = 'GoodDay', Value = 'Guten Tag!' -#> -function Import-LocStrings { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$LiteralPath) - - # Validate the file exists. - if (!(Test-Path -LiteralPath $LiteralPath -PathType Leaf)) { - Write-Warning (Get-LocString -Key PSLIB_FileNotFound0 -ArgumentList $LiteralPath) - return - } - - # Load the json. - Write-Verbose "Loading resource strings from: $LiteralPath" - $count = 0 - if ($messages = (Get-Content -LiteralPath $LiteralPath -Encoding UTF8 | Out-String | ConvertFrom-Json).messages) { - # Add each resource string to the hashtable. - foreach ($member in (Get-Member -InputObject $messages -MemberType NoteProperty)) { - [string]$key = $member.Name - $script:resourceStrings[$key] = $messages."$key" - $count++ - } - } - - Write-Verbose "Loaded $count strings." - - # Get the culture. - $culture = Get-TaskVariable -Name "System.Culture" -Default "en-US" - - # Load the resjson. - $resjsonPath = "$([System.IO.Path]::GetDirectoryName($LiteralPath))\Strings\resources.resjson\$culture\resources.resjson" - if (Test-Path -LiteralPath $resjsonPath) { - Write-Verbose "Loading resource strings from: $resjsonPath" - $count = 0 - $resjson = Get-Content -LiteralPath $resjsonPath -Encoding UTF8 | Out-String | ConvertFrom-Json - foreach ($member in (Get-Member -Name loc.messages.* -InputObject $resjson -MemberType NoteProperty)) { - if (!($value = $resjson."$($member.Name)")) { - continue - } - - [string]$key = $member.Name.Substring('loc.messages.'.Length) - $script:resourceStrings[$key] = $value - $count++ - } - - Write-Verbose "Loaded $count strings." - } -} diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 deleted file mode 100644 index 90400c7..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 +++ /dev/null @@ -1,634 +0,0 @@ -$script:loggingCommandPrefix = '##vso[' -$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: Add test to verify don't need to escape "=". - -$commandCorrelationId = $env:COMMAND_CORRELATION_ID -if ($null -ne $commandCorrelationId) -{ - [System.Environment]::SetEnvironmentVariable("COMMAND_CORRELATION_ID", $null) -} - -$IssueSources = @{ - CustomerScript = "CustomerScript" - TaskInternal = "TaskInternal" -} - -<# -.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-AddAttachment { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Type, - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$Path, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'addattachment' -Data $Path -Properties @{ - 'type' = $Type - 'name' = $Name - } -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-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. -#> -function Write-AddBuildTag { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Value, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'build' -Event 'addbuildtag' -Data $Value -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-AssociateArtifact { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$Type, - [hashtable]$Properties, - [switch]$AsOutput) - - $p = @{ } - if ($Properties) { - foreach ($key in $Properties.Keys) { - $p[$key] = $Properties[$key] - } - } - - $p['artifactname'] = $Name - $p['artifacttype'] = $Type - Write-LoggingCommand -Area 'artifact' -Event 'associate' -Data $Path -Properties $p -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-LogDetail { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [guid]$Id, - $ParentId, - [string]$Type, - [string]$Name, - $Order, - $StartTime, - $FinishTime, - $Progress, - [ValidateSet('Unknown', 'Initialized', 'InProgress', 'Completed')] - [Parameter()] - $State, - [ValidateSet('Succeeded', 'SucceededWithIssues', 'Failed', 'Cancelled', 'Skipped')] - [Parameter()] - $Result, - [string]$Message, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'logdetail' -Data $Message -Properties @{ - 'id' = $Id - 'parentid' = $ParentId - 'type' = $Type - 'name' = $Name - 'order' = $Order - 'starttime' = $StartTime - 'finishtime' = $FinishTime - 'progress' = $Progress - 'state' = $State - 'result' = $Result - } -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-SetProgress { - [CmdletBinding()] - param( - [ValidateRange(0, 100)] - [Parameter(Mandatory = $true)] - [int]$Percent, - [string]$CurrentOperation, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'setprogress' -Data $CurrentOperation -Properties @{ - 'value' = $Percent - } -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-SetResult { - [CmdletBinding(DefaultParameterSetName = 'AsOutput')] - param( - [ValidateSet("Succeeded", "SucceededWithIssues", "Failed", "Cancelled", "Skipped")] - [Parameter(Mandatory = $true)] - [string]$Result, - [string]$Message, - [Parameter(ParameterSetName = 'AsOutput')] - [switch]$AsOutput, - [Parameter(ParameterSetName = 'DoNotThrow')] - [switch]$DoNotThrow) - - Write-LoggingCommand -Area 'task' -Event 'complete' -Data $Message -Properties @{ - 'result' = $Result - } -AsOutput:$AsOutput - if ($Result -eq 'Failed' -and !$AsOutput -and !$DoNotThrow) { - # Special internal exception type to control the flow. Not currently intended - # for public usage and subject to change. - throw (New-Object VstsTaskSdk.TerminationException($Message)) - } -} - -<# -.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-SetSecret { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Value, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'setsecret' -Data $Value -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-SetVariable { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [string]$Value, - [switch]$Secret, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $Value -Properties @{ - 'variable' = $Name - 'issecret' = $Secret - } -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-TaskDebug { - [CmdletBinding()] - param( - [string]$Message, - [switch]$AsOutput) - - Write-TaskDebug_Internal @PSBoundParameters -} - -<# -.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-TaskError { - [CmdletBinding()] - param( - [string]$Message, - [string]$ErrCode, - [string]$SourcePath, - [string]$LineNumber, - [string]$ColumnNumber, - [switch]$AsOutput, - [string]$IssueSource, - [string]$AuditAction - ) - - Write-LogIssue -Type error @PSBoundParameters -} - -<# -.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-TaskVerbose { - [CmdletBinding()] - param( - [string]$Message, - [switch]$AsOutput) - - Write-TaskDebug_Internal @PSBoundParameters -AsVerbose -} - -<# -.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-TaskWarning { - [CmdletBinding()] - param( - [string]$Message, - [string]$ErrCode, - [string]$SourcePath, - [string]$LineNumber, - [string]$ColumnNumber, - [switch]$AsOutput, - [string]$IssueSource, - [string]$AuditAction - ) - - Write-LogIssue -Type warning @PSBoundParameters -} - -<# -.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. -#> -function Write-UpdateBuildNumber { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Value, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'build' -Event 'updatebuildnumber' -Data $Value -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-UploadArtifact { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$ContainerFolder, - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$Path, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'artifact' -Event 'upload' -Data $Path -Properties @{ - 'containerfolder' = $ContainerFolder - 'artifactname' = $Name - } -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-UploadBuildLog { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [switch]$AsOutput) - - 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 -} - -<# -.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-LoggingCommand { - [CmdletBinding(DefaultParameterSetName = 'Parameters')] - param( - [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] - [string]$Area, - [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] - [string]$Event, - [Parameter(ParameterSetName = 'Parameters')] - [string]$Data, - [Parameter(ParameterSetName = 'Parameters')] - [hashtable]$Properties, - [Parameter(Mandatory = $true, ParameterSetName = 'Object')] - $Command, - [switch]$AsOutput) - - if ($PSCmdlet.ParameterSetName -eq 'Object') { - Write-LoggingCommand -Area $Command.Area -Event $Command.Event -Data $Command.Data -Properties $Command.Properties -AsOutput:$AsOutput - return - } - - $command = Format-LoggingCommand -Area $Area -Event $Event -Data $Data -Properties $Properties - if ($AsOutput) { - $command - } else { - Write-Host $command - } -} - -######################################## -# Private functions. -######################################## -function Format-LoggingCommandData { - [CmdletBinding()] - param([string]$Value, [switch]$Reverse) - - if (!$Value) { - return '' - } - - if (!$Reverse) { - foreach ($mapping in $script:loggingCommandEscapeMappings) { - $Value = $Value.Replace($mapping.Token, $mapping.Replacement) - } - } else { - for ($i = $script:loggingCommandEscapeMappings.Length - 1 ; $i -ge 0 ; $i--) { - $mapping = $script:loggingCommandEscapeMappings[$i] - $Value = $Value.Replace($mapping.Replacement, $mapping.Token) - } - } - - return $Value -} - -function Format-LoggingCommand { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Area, - [Parameter(Mandatory = $true)] - [string]$Event, - [string]$Data, - [System.Collections.IDictionary]$Properties) - - # Append the preamble. - [System.Text.StringBuilder]$sb = New-Object -TypeName System.Text.StringBuilder - $null = $sb.Append($script:loggingCommandPrefix).Append($Area).Append('.').Append($Event) - - # Append the properties. - if ($Properties) { - $first = $true - foreach ($key in $Properties.Keys) { - [string]$value = Format-LoggingCommandData $Properties[$key] - if ($value) { - if ($first) { - $null = $sb.Append(' ') - $first = $false - } else { - $null = $sb.Append(';') - } - - $null = $sb.Append("$key=$value") - } - } - } - - # Append the tail and output the value. - $Data = Format-LoggingCommandData $Data - $sb.Append(']').Append($Data).ToString() -} - -function Write-LogIssue { - [CmdletBinding()] - param( - [ValidateSet('warning', 'error')] - [Parameter(Mandatory = $true)] - [string]$Type, - [string]$Message, - [string]$ErrCode, - [string]$SourcePath, - [string]$LineNumber, - [string]$ColumnNumber, - [switch]$AsOutput, - [AllowNull()] - [ValidateSet('CustomerScript', 'TaskInternal')] - [string]$IssueSource = $IssueSources.TaskInternal, - [string]$AuditAction - ) - - $properties = [ordered]@{ - 'type' = $Type - 'code' = $ErrCode - 'sourcepath' = $SourcePath - 'linenumber' = $LineNumber - 'columnnumber' = $ColumnNumber - 'source' = $IssueSource - 'correlationId' = $commandCorrelationId - 'auditAction' = $AuditAction - } - $command = Format-LoggingCommand -Area 'task' -Event 'logissue' -Data $Message -Properties $properties - if ($AsOutput) { - return $command - } - - if ($Type -eq 'error') { - $foregroundColor = $host.PrivateData.ErrorForegroundColor - $backgroundColor = $host.PrivateData.ErrorBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::Red - $backgroundColor = [System.ConsoleColor]::Black - } - } else { - $foregroundColor = $host.PrivateData.WarningForegroundColor - $backgroundColor = $host.PrivateData.WarningBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::Yellow - $backgroundColor = [System.ConsoleColor]::Black - } - } - - Write-Host $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor -} - -function Write-TaskDebug_Internal { - [CmdletBinding()] - param( - [string]$Message, - [switch]$AsVerbose, - [switch]$AsOutput) - - $command = Format-LoggingCommand -Area 'task' -Event 'debug' -Data $Message - if ($AsOutput) { - return $command - } - - if ($AsVerbose) { - $foregroundColor = $host.PrivateData.VerboseForegroundColor - $backgroundColor = $host.PrivateData.VerboseBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::Cyan - $backgroundColor = [System.ConsoleColor]::Black - } - } else { - $foregroundColor = $host.PrivateData.DebugForegroundColor - $backgroundColor = $host.PrivateData.DebugBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::DarkGray - $backgroundColor = [System.ConsoleColor]::Black - } - } - - Write-Host -Object $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor -} diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 deleted file mode 100644 index 51cda34..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 +++ /dev/null @@ -1,205 +0,0 @@ -######################################## -# Private functions. -######################################## -function ConvertFrom-LongFormPath { - [CmdletBinding()] - param([string]$Path) - - if ($Path) { - if ($Path.StartsWith('\\?\UNC')) { - # E.g. \\?\UNC\server\share -> \\server\share - return $Path.Substring(1, '\?\UNC'.Length) - } elseif ($Path.StartsWith('\\?\')) { - # E.g. \\?\C:\directory -> C:\directory - return $Path.Substring('\\?\'.Length) - } - } - - return $Path -} -function ConvertTo-LongFormPath { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path) - - [string]$longFormPath = Get-FullNormalizedPath -Path $Path - if ($longFormPath -and !$longFormPath.StartsWith('\\?')) { - if ($longFormPath.StartsWith('\\')) { - # E.g. \\server\share -> \\?\UNC\server\share - return "\\?\UNC$($longFormPath.Substring(1))" - } else { - # E.g. C:\directory -> \\?\C:\directory - return "\\?\$longFormPath" - } - } - - return $longFormPath -} - -# TODO: ADD A SWITCH TO EXCLUDE FILES, A SWITCH TO EXCLUDE DIRECTORIES, AND A SWITCH NOT TO FOLLOW REPARSE POINTS. -function Get-DirectoryChildItem { - [CmdletBinding()] - param( - [string]$Path, - [ValidateNotNullOrEmpty()] - [Parameter()] - [string]$Filter = "*", - [switch]$Force, - [VstsTaskSdk.FS.FindFlags]$Flags = [VstsTaskSdk.FS.FindFlags]::LargeFetch, - [VstsTaskSdk.FS.FindInfoLevel]$InfoLevel = [VstsTaskSdk.FS.FindInfoLevel]::Basic, - [switch]$Recurse) - - $stackOfDirectoryQueues = New-Object System.Collections.Stack - while ($true) { - $directoryQueue = New-Object System.Collections.Queue - $fileQueue = New-Object System.Collections.Queue - $findData = New-Object VstsTaskSdk.FS.FindData - $longFormPath = (ConvertTo-LongFormPath $Path) - $handle = $null - try { - $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( - [System.IO.Path]::Combine($longFormPath, $Filter), - $InfoLevel, - $findData, - [VstsTaskSdk.FS.FindSearchOps]::NameMatch, - [System.IntPtr]::Zero, - $Flags) - if (!$handle.IsInvalid) { - while ($true) { - if ($findData.fileName -notin '.', '..') { - $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes - # If the item is hidden, check if $Force is specified. - if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { - # Create the item. - $item = New-Object -TypeName psobject -Property @{ - 'Attributes' = $attributes - 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) - 'Name' = $findData.fileName - } - # Output directories immediately. - if ($item.Attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { - $item - # Append to the directory queue if recursive and default filter. - if ($Recurse -and $Filter -eq '*') { - $directoryQueue.Enqueue($item) - } - } else { - # Hold the files until all directories have been output. - $fileQueue.Enqueue($item) - } - } - } - - if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } - - if ($handle.IsInvalid) { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path - )) - } - } - } - } finally { - if ($handle -ne $null) { $handle.Dispose() } - } - - # If recursive and non-default filter, queue child directories. - if ($Recurse -and $Filter -ne '*') { - $findData = New-Object VstsTaskSdk.FS.FindData - $handle = $null - try { - $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( - [System.IO.Path]::Combine($longFormPath, '*'), - [VstsTaskSdk.FS.FindInfoLevel]::Basic, - $findData, - [VstsTaskSdk.FS.FindSearchOps]::NameMatch, - [System.IntPtr]::Zero, - $Flags) - if (!$handle.IsInvalid) { - while ($true) { - if ($findData.fileName -notin '.', '..') { - $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes - # If the item is hidden, check if $Force is specified. - if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { - # Collect directories only. - if ($attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { - # Create the item. - $item = New-Object -TypeName psobject -Property @{ - 'Attributes' = $attributes - 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) - 'Name' = $findData.fileName - } - $directoryQueue.Enqueue($item) - } - } - } - - if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } - - if ($handle.IsInvalid) { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path - )) - } - } - } - } finally { - if ($handle -ne $null) { $handle.Dispose() } - } - } - - # Output the files. - $fileQueue - - # Push the directory queue onto the stack if any directories were found. - if ($directoryQueue.Count) { $stackOfDirectoryQueues.Push($directoryQueue) } - - # Break out of the loop if no more directory queues to process. - if (!$stackOfDirectoryQueues.Count) { break } - - # Get the next path. - $directoryQueue = $stackOfDirectoryQueues.Peek() - $Path = $directoryQueue.Dequeue().FullName - - # Pop the directory queue if it's empty. - if (!$directoryQueue.Count) { $null = $stackOfDirectoryQueues.Pop() } - } -} - -function Get-FullNormalizedPath { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path) - - [string]$outPath = $Path - [uint32]$bufferSize = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, 0, $null, $null) - [int]$lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - if ($bufferSize -gt 0) { - $absolutePath = New-Object System.Text.StringBuilder([int]$bufferSize) - [uint32]$length = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, $bufferSize, $absolutePath, $null) - $lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - if ($length -gt 0) { - $outPath = $absolutePath.ToString() - } else { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - $lastWin32Error - Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path - )) - } - } else { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - $lastWin32Error - Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path - )) - } - - if ($outPath.EndsWith('\') -and !$outPath.EndsWith(':\')) { - $outPath = $outPath.TrimEnd('\') - } - - $outPath -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Minimatch.dll b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Minimatch.dll deleted file mode 100644 index 700ddc426e56b08270c9dcbb1ade8ded248f98d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18432 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 X?dtd6e<)43|K{fYzxw~TJn(-2*CUr_ diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/OutFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/OutFunctions.ps1 deleted file mode 100644 index ce9160b..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/OutFunctions.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -# TODO: It would be better if the Out-Default function resolved the underlying Out-Default -# command in the begin block. This would allow for supporting other modules that override -# Out-Default. -$script:outDefaultCmdlet = $ExecutionContext.InvokeCommand.GetCmdlet("Microsoft.PowerShell.Core\Out-Default") - -######################################## -# Public functions. -######################################## -function Out-Default { - [CmdletBinding(ConfirmImpact = "Medium")] - param( - [Parameter(ValueFromPipeline = $true)] - [System.Management.Automation.PSObject]$InputObject) - - begin { - #Write-Host '[Entering Begin Out-Default]' - $__sp = { & $script:outDefaultCmdlet @PSBoundParameters }.GetSteppablePipeline() - $__sp.Begin($pscmdlet) - #Write-Host '[Leaving Begin Out-Default]' - } - - process { - #Write-Host '[Entering Process Out-Default]' - if ($_ -is [System.Management.Automation.ErrorRecord]) { - Write-Verbose -Message 'Error record:' 4>&1 | Out-Default - Write-Verbose -Message (Remove-TrailingNewLine (Out-String -InputObject $_ -Width 2147483647)) 4>&1 | Out-Default - Write-Verbose -Message 'Script stack trace:' 4>&1 | Out-Default - Write-Verbose -Message "$($_.ScriptStackTrace)" 4>&1 | Out-Default - Write-Verbose -Message 'Exception:' 4>&1 | Out-Default - Write-Verbose -Message $_.Exception.ToString() 4>&1 | Out-Default - Write-TaskError -Message $_.Exception.Message -IssueSource $IssueSources.TaskInternal - } elseif ($_ -is [System.Management.Automation.WarningRecord]) { - Write-TaskWarning -Message (Remove-TrailingNewLine (Out-String -InputObject $_ -Width 2147483647)) -IssueSource $IssueSources.TaskInternal - } elseif ($_ -is [System.Management.Automation.VerboseRecord] -and !$global:__vstsNoOverrideVerbose) { - foreach ($private:str in (Format-DebugMessage -Object $_)) { - Write-TaskVerbose -Message $private:str - } - } elseif ($_ -is [System.Management.Automation.DebugRecord] -and !$global:__vstsNoOverrideVerbose) { - foreach ($private:str in (Format-DebugMessage -Object $_)) { - Write-TaskDebug -Message $private:str - } - } else { -# TODO: Consider using out-string here to control the width. As a security precaution it would actually be best to set it to max so wrapping doesn't interfere with secret masking. - $__sp.Process($_) - } - - #Write-Host '[Leaving Process Out-Default]' - } - - end { - #Write-Host '[Entering End Out-Default]' - $__sp.End() - #Write-Host '[Leaving End Out-Default]' - } -} - -######################################## -# Private functions. -######################################## -function Format-DebugMessage { - [CmdletBinding()] - param([psobject]$Object) - - $private:str = Out-String -InputObject $Object -Width 2147483647 - $private:str = Remove-TrailingNewLine $private:str - "$private:str".Replace("`r`n", "`n").Replace("`r", "`n").Split("`n"[0]) -} - -function Remove-TrailingNewLine { - [CmdletBinding()] - param($Str) - if ([object]::ReferenceEquals($Str, $null)) { - return $Str - } elseif ($Str.EndsWith("`r`n")) { - return $Str.Substring(0, $Str.Length - 2) - } else { - return $Str - } -} diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml deleted file mode 100644 index d930995..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - Microsoft.PowerShell.Commands.PSRepositoryItemInfo - System.Management.Automation.PSCustomObject - System.Object - - - VstsTaskSdk - 0.21.0 - Module - VSTS Task SDK - Microsoft Corporation - - - System.Object[] - System.Array - System.Object - - - VSTS - martinmrazik - EDergachev - tramsing - - - (c) Microsoft Corporation. All rights reserved. -
2024-05-02T10:19:39-06:00
- - - - https://github.com/Microsoft/azure-pipelines-task-lib - - - - - PSModule - - - - - System.Collections.Hashtable - System.Object - - - - Command - - - - - - - DscResource - - - - RoleCapability - - - - Function - - - - Workflow - - - - Cmdlet - - - - - - - - - - - https://www.powershellgallery.com/api/v2 - PSGallery - NuGet - - - System.Management.Automation.PSCustomObject - System.Object - - - (c) Microsoft Corporation. All rights reserved. - VSTS Task SDK - False - True - True - 88436 - 684804 - 67529 - 5/2/2024 10:19:39 -06:00 - 5/2/2024 10:19:39 -06:00 - 5/22/2025 21:30:00 -06:00 - PSModule - False - 2025-05-22T21:30:00Z - 0.21.0 - Microsoft Corporation - false - Module - VstsTaskSdk.nuspec|FindFunctions.ps1|LegacyFindFunctions.ps1|LocalizationFunctions.ps1|LongPathFunctions.ps1|OutFunctions.ps1|ToolFunctions.ps1|VstsTaskSdk.dll|VstsTaskSdk.psd1|Strings\resources.resjson\de-DE\resources.resjson|Strings\resources.resjson\es-ES\resources.resjson|Strings\resources.resjson\it-IT\resources.resjson|Strings\resources.resjson\ja-JP\resources.resjson|Strings\resources.resjson\ko-KR\resources.resjson|Strings\resources.resjson\ru-RU\resources.resjson|Strings\resources.resjson\zh-CN\resources.resjson|Strings\resources.resjson\zh-TW\resources.resjson|InputFunctions.ps1|lib.json|LoggingCommandFunctions.ps1|Minimatch.dll|ServerOMFunctions.ps1|TraceFunctions.ps1|VstsTaskSdk.psm1|Strings\resources.resjson\en-US\resources.resjson|Strings\resources.resjson\fr-FR\resources.resjson - bbed04e2-4e8e-4089-90a2-58b858fed8d8 - 3.0 - Microsoft Corporation - - - D:\Work\GitHub\bcdevopsextension\temp\VstsTaskSdk\0.21.0 -
-
-
diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 deleted file mode 100644 index 6fd19ea..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 +++ /dev/null @@ -1,684 +0,0 @@ -<# -.SYNOPSIS -Gets assembly reference information. - -.DESCRIPTION -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. - -Walks an assembly's references to determine all of it's dependencies. Also walks the references of the dependencies, and so on until all nested dependencies have been traversed. Dependencies are searched for in the directory of the specified assembly. NET Framework assemblies are omitted. - -See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER LiteralPath -Assembly to walk. - -.EXAMPLE -Get-VstsAssemblyReference -LiteralPath C:\nuget\microsoft.teamfoundationserver.client.14.102.0\lib\net45\Microsoft.TeamFoundation.Build2.WebApi.dll -#> -function Get-AssemblyReference { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$LiteralPath) - - $ErrorActionPreference = 'Stop' - 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) - $hashtable = @{ } - $queue = @( [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($LiteralPath).GetName() ) - while ($queue.Count) { - # Add a blank line between assemblies. - Write-Output '' - - # Pop. - $assemblyName = $queue[0] - $queue = @( $queue | Select-Object -Skip 1 ) - - # Attempt to find the assembly in the same directory. - $assembly = $null - $path = "$directory\$($assemblyName.Name).dll" - if ((Test-Path -LiteralPath $path -PathType Leaf)) { - $assembly = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($path) - } else { - $path = "$directory\$($assemblyName.Name).exe" - if ((Test-Path -LiteralPath $path -PathType Leaf)) { - $assembly = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($path) - } - } - - # Make sure the assembly full name matches, not just the file name. - if ($assembly -and $assembly.GetName().FullName -ne $assemblyName.FullName) { - $assembly = $null - } - - # Print the assembly. - if ($assembly) { - Write-Output $assemblyName.FullName - } else { - if ($assemblyName.FullName -eq 'Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed') { - Write-Warning "*** NOT FOUND $($assemblyName.FullName) *** This is an expected condition when using the HTTP clients from the 15.x VSTS REST SDK. Use Get-VstsVssHttpClient to load the HTTP clients (which applies a binding redirect assembly resolver for Newtonsoft.Json). Otherwise you will need to manage the binding redirect yourself." - } else { - Write-Warning "*** NOT FOUND $($assemblyName.FullName) ***" - } - - continue - } - - # Walk the references. - $refAssemblyNames = @( $assembly.GetReferencedAssemblies() ) - for ($i = 0 ; $i -lt $refAssemblyNames.Count ; $i++) { - $refAssemblyName = $refAssemblyNames[$i] - - # Skip framework assemblies. - $fxPaths = @( - "$env:windir\Microsoft.Net\Framework64\v4.0.30319\$($refAssemblyName.Name).dll" - "$env:windir\Microsoft.Net\Framework64\v4.0.30319\WPF\$($refAssemblyName.Name).dll" - ) - $fxPath = $fxPaths | - Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | - Where-Object { [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($_).GetName().FullName -eq $refAssemblyName.FullName } - if ($fxPath) { - continue - } - - # Print the reference. - Write-Output " $($refAssemblyName.FullName)" - - # Add new references to the queue. - if (!$hashtable[$refAssemblyName.FullName]) { - $queue += $refAssemblyName - $hashtable[$refAssemblyName.FullName] = $true - } - } - } -} - -<# -.SYNOPSIS -Gets a credentials object that can be used with the TFS extended client SDK. - -.DESCRIPTION -The agent job token is used 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). - -Refer to Get-VstsTfsService for a more simple to get a TFS service object. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER OMDirectory -Directory where the extended client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.EXAMPLE -# -# Refer to Get-VstsTfsService for a more simple way to get a TFS service object. -# -$credentials = Get-VstsTfsClientCredentials -Add-Type -LiteralPath "$PSScriptRoot\Microsoft.TeamFoundation.VersionControl.Client.dll" -$tfsTeamProjectCollection = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection( - (Get-VstsTaskVariable -Name 'System.TeamFoundationCollectionUri' -Require), - $credentials) -$versionControlServer = $tfsTeamProjectCollection.GetService([Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer]) -$versionControlServer.GetItems('$/*').Items | Format-List -#> -function Get-TfsClientCredentials { - [CmdletBinding()] - param([string]$OMDirectory) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Get the endpoint. - $endpoint = Get-Endpoint -Name SystemVssConnection -Require - - # Test if the Newtonsoft.Json DLL exists in the OM directory. - $newtonsoftDll = [System.IO.Path]::Combine($OMDirectory, "Newtonsoft.Json.dll") - Write-Verbose "Testing file path: '$newtonsoftDll'" - if (!(Test-Path -LiteralPath $newtonsoftDll -PathType Leaf)) { - Write-Verbose 'Not found. Rethrowing exception.' - throw - } - - # Add a binding redirect and try again. Parts of the Dev15 preview SDK have a - # 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] { - param($sender, $e) - - if ($e.Name -like 'Newtonsoft.Json, *') { - Write-Verbose "Resolving '$($e.Name)' to '$newtonsoftDll'." - - return [System.Reflection.Assembly]::LoadFrom($newtonsoftDll) - } - - return $null - } - [System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolve) - - # Validate the type can be found. - $null = Get-OMType -TypeName 'Microsoft.TeamFoundation.Client.TfsClientCredentials' -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require - - # Construct the credentials. - $credentials = New-Object Microsoft.TeamFoundation.Client.TfsClientCredentials($false) # Do not use default credentials. - $credentials.AllowInteractive = $false - $credentials.Federated = New-Object Microsoft.TeamFoundation.Client.OAuthTokenCredential([string]$endpoint.auth.parameters.AccessToken) - return $credentials - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Gets a TFS extended client service. - -.DESCRIPTION -Gets an instance of an ITfsTeamProjectCollectionObject. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER TypeName -Namespace-qualified type name of the service to get. - -.PARAMETER OMDirectory -Directory where the extended client object model DLLs are located. If the DLLs for the types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER Uri -URI to use when initializing the service. If not specified, defaults to System.TeamFoundationCollectionUri. - -.PARAMETER TfsClientCredentials -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 -$versionControlServer.GetItems('$/*').Items | Format-List -#> -function Get-TfsService { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TypeName, - - [string]$OMDirectory, - - [string]$Uri, - - $TfsClientCredentials) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Default the URI to the collection URI. - if (!$Uri) { - $Uri = Get-TaskVariable -Name System.TeamFoundationCollectionUri -Require - } - - # Default the credentials. - if (!$TfsClientCredentials) { - $TfsClientCredentials = Get-TfsClientCredentials -OMDirectory $OMDirectory - } - - # Validate the project collection type can be loaded. - $null = Get-OMType -TypeName 'Microsoft.TeamFoundation.Client.TfsTeamProjectCollection' -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require - - # Load the project collection object. - $tfsTeamProjectCollection = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection($Uri, $TfsClientCredentials) - - # Validate the requested type can be loaded. - $type = Get-OMType -TypeName $TypeName -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require - - # Return the service object. - return $tfsTeamProjectCollection.GetService($type) - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Gets a credentials object that can be used with the VSTS REST SDK. - -.DESCRIPTION -The agent job token is used 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 service build/release identity). - -Refer to Get-VstsVssHttpClient for a more simple to get a VSS HTTP client. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -.PARAMETER OMDirectory -Directory where the REST client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -.EXAMPLE -# -# Refer to Get-VstsTfsService for a more simple way to get a TFS service object. -# -# This example works using the 14.x .NET SDK. A Newtonsoft.Json 6.0 to 8.0 binding -# redirect may be required when working with the 15.x SDK. Or use Get-VstsVssHttpClient -# to avoid managing the binding redirect. -# -$vssCredentials = Get-VstsVssCredentials -$collectionUrl = New-Object System.Uri((Get-VstsTaskVariable -Name 'System.TeamFoundationCollectionUri' -Require)) -Add-Type -LiteralPath "$PSScriptRoot\Microsoft.TeamFoundation.Core.WebApi.dll" -$projectHttpClient = New-Object Microsoft.TeamFoundation.Core.WebApi.ProjectHttpClient($collectionUrl, $vssCredentials) -$projectHttpClient.GetProjects().Result -#> -function Get-VssCredentials { - [CmdletBinding()] - param([string]$OMDirectory) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Get the endpoint. - $endpoint = Get-Endpoint -Name SystemVssConnection -Require - - # Check if the VssOAuthAccessTokenCredential type is available. - if ((Get-OMType -TypeName 'Microsoft.VisualStudio.Services.OAuth.VssOAuthAccessTokenCredential' -OMKind 'WebApi' -OMDirectory $OMDirectory)) { - # Create the federated credential. - $federatedCredential = New-Object Microsoft.VisualStudio.Services.OAuth.VssOAuthAccessTokenCredential($endpoint.auth.parameters.AccessToken) - } else { - # Validate the fallback type can be loaded. - $null = Get-OMType -TypeName 'Microsoft.VisualStudio.Services.Client.VssOAuthCredential' -OMKind 'WebApi' -OMDirectory $OMDirectory -Require - - # Create the federated credential. - $federatedCredential = New-Object Microsoft.VisualStudio.Services.Client.VssOAuthCredential($endpoint.auth.parameters.AccessToken) - } - - # Return the credentials. - return New-Object Microsoft.VisualStudio.Services.Common.VssCredentials( - (New-Object Microsoft.VisualStudio.Services.Common.WindowsCredential($false)), # Do not use default credentials. - $federatedCredential, - [Microsoft.VisualStudio.Services.Common.CredentialPromptType]::DoNotPrompt) - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Gets a VSS HTTP client. - -.DESCRIPTION -Gets an instance of an VSS HTTP client. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -.PARAMETER TypeName -Namespace-qualified type name of the HTTP client to get. - -.PARAMETER OMDirectory -Directory where the REST client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -# .PARAMETER Uri -# URI to use when initializing the HTTP client. If not specified, defaults to System.TeamFoundationCollectionUri. - -# .PARAMETER VssCredentials -# 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 -$projectHttpClient.GetProjects().Result -#> -function Get-VssHttpClient { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TypeName, - - [string]$OMDirectory, - - [string]$Uri, - - $VssCredentials, - - $WebProxy = (Get-WebProxy), - - $ClientCert = (Get-ClientCertificate), - - [switch]$IgnoreSslError) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Default the URI to the collection URI. - if (!$Uri) { - $Uri = Get-TaskVariable -Name System.TeamFoundationCollectionUri -Require - } - - # Cast the URI. - [uri]$Uri = New-Object System.Uri($Uri) - - # Default the credentials. - if (!$VssCredentials) { - $VssCredentials = Get-VssCredentials -OMDirectory $OMDirectory - } - - # 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, $Settings) - } catch { - # Rethrow if the exception is not due to Newtonsoft.Json DLL not found. - if ($_.Exception.InnerException -isnot [System.IO.FileNotFoundException] -or - $_.Exception.InnerException.FileName -notlike 'Newtonsoft.Json, *') { - - throw - } - - # Default the OMDirectory to the directory of the entry script for the task. - if (!$OMDirectory) { - $OMDirectory = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") - Write-Verbose "Defaulted OM directory to: '$OMDirectory'" - } - - # Test if the Newtonsoft.Json DLL exists in the OM directory. - $newtonsoftDll = [System.IO.Path]::Combine($OMDirectory, "Newtonsoft.Json.dll") - Write-Verbose "Testing file path: '$newtonsoftDll'" - if (!(Test-Path -LiteralPath $newtonsoftDll -PathType Leaf)) { - Write-Verbose 'Not found. Rethrowing exception.' - throw - } - - # Add a binding redirect and try again. Parts of the Dev15 preview SDK have a - # 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] { - param($sender, $e) - - if ($e.Name -like 'Newtonsoft.Json, *') { - Write-Verbose "Resolving '$($e.Name)'" - return [System.Reflection.Assembly]::LoadFrom($newtonsoftDll) - } - - Write-Verbose "Unable to resolve assembly name '$($e.Name)'" - return $null - } - [System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolve) - try { - # Try again to construct the HTTP client. - Write-Verbose "Trying again to construct the HTTP client." - return New-Object $TypeName($Uri, $VssCredentials, $Settings) - } finally { - # Unregister the assembly resolver. - Write-Verbose "Removing assemlby resolver." - [System.AppDomain]::CurrentDomain.remove_AssemblyResolve($onAssemblyResolve) - } - } - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.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/azure-pipelines-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. -######################################## -function Get-OMType { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TypeName, - - [ValidateSet('ExtendedClient', 'WebApi')] - [Parameter(Mandatory = $true)] - [string]$OMKind, - - [string]$OMDirectory, - - [switch]$Require) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - try { - # Default the OMDirectory to the directory of the entry script for the task. - if (!$OMDirectory) { - $OMDirectory = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") - Write-Verbose "Defaulted OM directory to: '$OMDirectory'" - } - - # Try to load the type. - $errorRecord = $null - Write-Verbose "Testing whether type can be loaded: '$TypeName'" - $ErrorActionPreference = 'Ignore' - try { - # Failure when attempting to cast a string to a type, transfers control to the - # catch handler even when the error action preference is ignore. The error action - # is set to Ignore so the $Error variable is not polluted. - $type = [type]$TypeName - - # Success. - Write-Verbose "The type was loaded successfully." - return $type - } catch { - # Store the error record. - $errorRecord = $_ - } - - $ErrorActionPreference = 'Stop' - Write-Verbose "The type was not loaded." - - # Build a list of candidate DLL file paths from the namespace. - $dllPaths = @( ) - $namespace = $TypeName - while ($namespace.LastIndexOf('.') -gt 0) { - # Trim the next segment from the namespace. - $namespace = $namespace.SubString(0, $namespace.LastIndexOf('.')) - - # Derive potential DLL file paths based on the namespace and OM kind (i.e. extended client vs web API). - if ($OMKind -eq 'ExtendedClient') { - if ($namespace -like 'Microsoft.TeamFoundation.*') { - $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.dll") - } - } else { - if ($namespace -like 'Microsoft.TeamFoundation.*' -or - $namespace -like 'Microsoft.VisualStudio.Services' -or - $namespace -like 'Microsoft.VisualStudio.Services.*') { - - $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.WebApi.dll") - $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.dll") - } - } - } - - foreach ($dllPath in $dllPaths) { - # Check whether the DLL exists. - Write-Verbose "Testing leaf path: '$dllPath'" - if (!(Test-Path -PathType Leaf -LiteralPath "$dllPath")) { - Write-Verbose "Not found." - continue - } - - # Load the DLL. - Write-Verbose "Loading assembly: $dllPath" - try { - Add-Type -LiteralPath $dllPath - } catch { - # Write the information to the verbose stream and proceed to attempt to load the requested type. - # - # The requested type may successfully load now. For example, the type used with the 14.0 Web API for the - # federated credential (VssOAuthCredential) resides in Microsoft.VisualStudio.Services.Client.dll. Even - # though loading the DLL results in a ReflectionTypeLoadException when Microsoft.ServiceBus.dll (approx 3.75mb) - # is not present, enough types are loaded to use the VssOAuthCredential federated credential with the Web API - # HTTP clients. - Write-Verbose "$($_.Exception.GetType().FullName): $($_.Exception.Message)" - if ($_.Exception -is [System.Reflection.ReflectionTypeLoadException]) { - for ($i = 0 ; $i -lt $_.Exception.LoaderExceptions.Length ; $i++) { - $loaderException = $_.Exception.LoaderExceptions[$i] - Write-Verbose "LoaderExceptions[$i]: $($loaderException.GetType().FullName): $($loaderException.Message)" - } - } - } - - # Try to load the type. - Write-Verbose "Testing whether type can be loaded: '$TypeName'" - $ErrorActionPreference = 'Ignore' - try { - # Failure when attempting to cast a string to a type, transfers control to the - # catch handler even when the error action preference is ignore. The error action - # is set to Ignore so the $Error variable is not polluted. - $type = [type]$TypeName - - # Success. - Write-Verbose "The type was loaded successfully." - return $type - } catch { - $errorRecord = $_ - } - - $ErrorActionPreference = 'Stop' - Write-Verbose "The type was not loaded." - } - - # Check whether to propagate the error. - if ($Require) { - Write-Error $errorRecord - } - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson deleted file mode 100644 index 248b674..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Fehler beim Aufzählen von Unterverzeichnissen für den folgenden Pfad: \"{0}\"", - "loc.messages.PSLIB_FileNotFound0": "Die Datei wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_Input0": "\"{0}\"-Eingabe", - "loc.messages.PSLIB_InvalidPattern0": "Ungültiges Muster: \"{0}\"", - "loc.messages.PSLIB_LeafPathNotFound0": "Der Blattpfad wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Fehler bei der Normalisierung bzw. Erweiterung des Pfads. Die Pfadlänge wurde vom Kernel32-Subsystem nicht zurückgegeben für: \"{0}\"", - "loc.messages.PSLIB_PathNotFound0": "Der Pfad wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Der Prozess \"{0}\" wurde mit dem Code \"{1}\" beendet.", - "loc.messages.PSLIB_Required0": "Erforderlich: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Fehler beim Zeichenfolgenformat.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "Der Zeichenfolgen-Ressourcenschlüssel wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_TaskVariable0": "\"{0}\"-Taskvariable" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson deleted file mode 100644 index 66c17bc..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Enumerating subdirectories failed for path: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "File not found: '{0}'", - "loc.messages.PSLIB_Input0": "'{0}' input", - "loc.messages.PSLIB_InvalidPattern0": "Invalid pattern: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Leaf path not found: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Path normalization/expansion failed. The path length was not returned by the Kernel32 subsystem for: '{0}'", - "loc.messages.PSLIB_PathNotFound0": "Path not found: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Process '{0}' exited with code '{1}'.", - "loc.messages.PSLIB_Required0": "Required: {0}", - "loc.messages.PSLIB_StringFormatFailed": "String format failed.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "String resource key not found: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' task variable" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson deleted file mode 100644 index b79ac21..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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}'", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "No se pudieron enumerar los subdirectorios de la ruta de acceso: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "Archivo no encontrado: '{0}'", - "loc.messages.PSLIB_Input0": "Entrada '{0}'", - "loc.messages.PSLIB_InvalidPattern0": "Patrón no válido: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "No se encuentra la ruta de acceso de la hoja: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "No se pudo normalizar o expandir la ruta de acceso. El subsistema Kernel32 no devolvió la longitud de la ruta de acceso para: '{0}'", - "loc.messages.PSLIB_PathNotFound0": "No se encuentra la ruta de acceso: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "El proceso '{0}' finalizó con el código '{1}'.", - "loc.messages.PSLIB_Required0": "Se requiere: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Error de formato de cadena.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "No se encuentra la clave de recurso de la cadena: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "Variable de tarea '{0}'" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson deleted file mode 100644 index dc2da05..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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}'", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Échec de l'énumération des sous-répertoires pour le chemin : '{0}'", - "loc.messages.PSLIB_FileNotFound0": "Fichier introuvable : {0}.", - "loc.messages.PSLIB_Input0": "Entrée '{0}'", - "loc.messages.PSLIB_InvalidPattern0": "Modèle non valide : '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Le chemin feuille est introuvable : '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Échec de la normalisation/l'expansion du chemin. La longueur du chemin n'a pas été retournée par le sous-système Kernel32 pour : '{0}'", - "loc.messages.PSLIB_PathNotFound0": "Chemin introuvable : '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Le processus '{0}' s'est arrêté avec le code '{1}'.", - "loc.messages.PSLIB_Required0": "Obligatoire : {0}", - "loc.messages.PSLIB_StringFormatFailed": "Échec du format de la chaîne.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "Clé de la ressource de type chaîne introuvable : '{0}'", - "loc.messages.PSLIB_TaskVariable0": "Variable de tâche '{0}'" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson deleted file mode 100644 index 77bea53..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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}'", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "L'enumerazione delle sottodirectory per il percorso '{0}' non è riuscita", - "loc.messages.PSLIB_FileNotFound0": "File non trovato: '{0}'", - "loc.messages.PSLIB_Input0": "Input di '{0}'", - "loc.messages.PSLIB_InvalidPattern0": "Criterio non valido: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Percorso foglia non trovato: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "La normalizzazione o l'espansione del percorso non è riuscita. Il sottosistema Kernel32 non ha restituito la lunghezza del percorso per '{0}'", - "loc.messages.PSLIB_PathNotFound0": "Percorso non trovato: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Il processo '{0}' è stato terminato ed è stato restituito il codice '{1}'.", - "loc.messages.PSLIB_Required0": "Obbligatorio: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Errore nel formato della stringa.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "La chiave della risorsa stringa non è stata trovata: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "Variabile dell'attività '{0}'" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson deleted file mode 100644 index 9f2f9fe..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "バージョン {0} 以降のエージェントが必要です。", - "loc.messages.PSLIB_ContainerPathNotFound0": "コンテナーのパスが見つかりません: '{0}'", - "loc.messages.PSLIB_EndpointAuth0": "'{0}' サービス エンドポイントの資格情報", - "loc.messages.PSLIB_EndpointUrl0": "'{0}' サービス エンドポイントの URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "パス '{0}' のサブディレクトリを列挙できませんでした", - "loc.messages.PSLIB_FileNotFound0": "ファイルが見つかりません: '{0}'", - "loc.messages.PSLIB_Input0": "'{0}' 入力", - "loc.messages.PSLIB_InvalidPattern0": "使用できないパターンです: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "リーフ パスが見つかりません: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "パスの正規化/展開に失敗しました。Kernel32 サブシステムからパス '{0}' の長さが返されませんでした", - "loc.messages.PSLIB_PathNotFound0": "パスが見つかりません: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "プロセス '{0}' がコード '{1}' で終了しました。", - "loc.messages.PSLIB_Required0": "必要: {0}", - "loc.messages.PSLIB_StringFormatFailed": "文字列のフォーマットに失敗しました。", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "文字列のリソース キーが見つかりません: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' タスク変数" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson deleted file mode 100644 index d809a2e..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "에이전트 버전 {0} 이상이 필요합니다.", - "loc.messages.PSLIB_ContainerPathNotFound0": "컨테이너 경로를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_EndpointAuth0": "'{0}' 서비스 엔드포인트 자격 증명", - "loc.messages.PSLIB_EndpointUrl0": "'{0}' 서비스 엔드포인트 URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "경로에 대해 하위 디렉터리를 열거하지 못함: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "{0} 파일을 찾을 수 없습니다.", - "loc.messages.PSLIB_Input0": "'{0}' 입력", - "loc.messages.PSLIB_InvalidPattern0": "잘못된 패턴: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Leaf 경로를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "경로 정규화/확장에 실패했습니다. 다음에 대해 Kernel32 subsystem에서 경로 길이를 반환하지 않음: '{0}'", - "loc.messages.PSLIB_PathNotFound0": "경로를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "'{1}' 코드로 '{0}' 프로세스가 종료되었습니다.", - "loc.messages.PSLIB_Required0": "필수: {0}", - "loc.messages.PSLIB_StringFormatFailed": "문자열을 포맷하지 못했습니다.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "문자열 리소스 키를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' 작업 변수" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson deleted file mode 100644 index 40daae7..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "Требуется версия агента {0} или более поздняя.", - "loc.messages.PSLIB_ContainerPathNotFound0": "Путь к контейнеру не найден: \"{0}\".", - "loc.messages.PSLIB_EndpointAuth0": "Учетные данные конечной точки службы \"{0}\"", - "loc.messages.PSLIB_EndpointUrl0": "URL-адрес конечной точки службы \"{0}\"", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Сбой перечисления подкаталогов для пути: \"{0}\".", - "loc.messages.PSLIB_FileNotFound0": "Файл не найден: \"{0}\"", - "loc.messages.PSLIB_Input0": "Входные данные \"{0}\".", - "loc.messages.PSLIB_InvalidPattern0": "Недопустимый шаблон: \"{0}\".", - "loc.messages.PSLIB_LeafPathNotFound0": "Путь к конечному объекту не найден: \"{0}\".", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Сбой нормализации и расширения пути. Длина пути не была возвращена подсистемой Kernel32 для: \"{0}\".", - "loc.messages.PSLIB_PathNotFound0": "Путь не найден: \"{0}\".", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Процесс \"{0}\" завершил работу с кодом \"{1}\".", - "loc.messages.PSLIB_Required0": "Требуется: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Сбой формата строки.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "Ключ ресурса строки не найден: \"{0}\".", - "loc.messages.PSLIB_TaskVariable0": "Переменная задачи \"{0}\"" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson deleted file mode 100644 index 49d824b..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "需要代理版本 {0} 或更高版本。", - "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路径:“{0}”", - "loc.messages.PSLIB_EndpointAuth0": "“{0}”服务终结点凭据", - "loc.messages.PSLIB_EndpointUrl0": "“{0}”服务终结点 URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "枚举路径的子目录失败:“{0}”", - "loc.messages.PSLIB_FileNotFound0": "找不到文件: {0}。", - "loc.messages.PSLIB_Input0": "“{0}”输入", - "loc.messages.PSLIB_InvalidPattern0": "无效的模式:“{0}”", - "loc.messages.PSLIB_LeafPathNotFound0": "找不到叶路径:“{0}”", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "路径规范化/扩展失败。路径长度不是由“{0}”的 Kernel32 子系统返回的", - "loc.messages.PSLIB_PathNotFound0": "找不到路径:“{0}”", - "loc.messages.PSLIB_Process0ExitedWithCode1": "过程“{0}”已退出,代码为“{1}”。", - "loc.messages.PSLIB_Required0": "必需: {0}", - "loc.messages.PSLIB_StringFormatFailed": "字符串格式无效。", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "找不到字符串资源关键字:“{0}”", - "loc.messages.PSLIB_TaskVariable0": "“{0}”任务变量" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson deleted file mode 100644 index 7cbf22e..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "需要代理程式版本 {0} 或更新的版本。", - "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路徑: '{0}'", - "loc.messages.PSLIB_EndpointAuth0": "'{0}' 服務端點認證", - "loc.messages.PSLIB_EndpointUrl0": "'{0}' 服務端點 URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "為路徑列舉子目錄失敗: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "找不到檔案: '{0}'", - "loc.messages.PSLIB_Input0": "'{0}' 輸入", - "loc.messages.PSLIB_InvalidPattern0": "模式無效: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "找不到分葉路徑: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "路徑正規化/展開失敗。Kernel32 子系統未傳回 '{0}' 的路徑長度", - "loc.messages.PSLIB_PathNotFound0": "找不到路徑: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "處理序 '{0}' 以返回碼 '{1}' 結束。", - "loc.messages.PSLIB_Required0": "必要項: {0}", - "loc.messages.PSLIB_StringFormatFailed": "字串格式失敗。", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "找不到字串資源索引鍵: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' 工作變數" -} \ No newline at end of file diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/ToolFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/ToolFunctions.ps1 deleted file mode 100644 index bb6f8d6..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/ToolFunctions.ps1 +++ /dev/null @@ -1,227 +0,0 @@ -<# -.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. - -.PARAMETER PassThru -True to return the path. -#> -function Assert-Path { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$LiteralPath, - [Microsoft.PowerShell.Commands.TestPathType]$PathType = [Microsoft.PowerShell.Commands.TestPathType]::Any, - [switch]$PassThru) - - if ($PathType -eq [Microsoft.PowerShell.Commands.TestPathType]::Any) { - Write-Verbose "Asserting path exists: '$LiteralPath'" - } - else { - Write-Verbose "Asserting $("$PathType".ToLowerInvariant()) path exists: '$LiteralPath'" - } - - if (Test-Path -LiteralPath $LiteralPath -PathType $PathType) { - if ($PassThru) { - return $LiteralPath - } - - return - } - - $resourceKey = switch ($PathType) { - ([Microsoft.PowerShell.Commands.TestPathType]::Container) { "PSLIB_ContainerPathNotFound0" ; break } - ([Microsoft.PowerShell.Commands.TestPathType]::Leaf) { "PSLIB_LeafPathNotFound0" ; break } - default { "PSLIB_PathNotFound0" } - } - - throw (Get-LocString -Key $resourceKey -ArgumentList $LiteralPath) -} - -<# -.SYNOPSIS -Executes an external program. - -.DESCRIPTION -Executes an external program and waits for the process to exit. - -After calling this command, the exit code of the process can be retrieved from the variable $LASTEXITCODE. - -.PARAMETER Encoding -This parameter not required for most scenarios. Indicates how to interpret the encoding from the external program. An example use case would be if an external program outputs UTF-16 XML and the output needs to be parsed. - -.PARAMETER RequireExitCodeZero -Indicates whether to write an error to the error pipeline if the exit code is not zero. -#> -function Invoke-Tool { - [CmdletBinding()] - param( - [ValidatePattern('^[^\r\n]*$')] - [Parameter(Mandatory = $true)] - [string]$FileName, - [ValidatePattern('^[^\r\n]*$')] - [Parameter()] - [string]$Arguments, - [string]$WorkingDirectory, - [System.Text.Encoding]$Encoding, - [switch]$RequireExitCodeZero, - [bool]$IgnoreHostException) - - Trace-EnteringInvocation $MyInvocation - $isPushed = $false - $originalEncoding = $null - try { - if ($Encoding) { - $originalEncoding = [System.Console]::OutputEncoding - [System.Console]::OutputEncoding = $Encoding - } - - if ($WorkingDirectory) { - Push-Location -LiteralPath $WorkingDirectory -ErrorAction Stop - $isPushed = $true - } - - $FileName = $FileName.Replace('"', '').Replace("'", "''") - Write-Host "##[command]""$FileName"" $Arguments" - try { - Invoke-Expression "& '$FileName' --% $Arguments" - } - catch [System.Management.Automation.Host.HostException] { - if ($IgnoreHostException -eq $False) { - throw - } - - Write-Host "##[warning]Host Exception was thrown by Invoke-Expression, suppress it due IgnoreHostException setting" - } - Write-Verbose "Exit code: $LASTEXITCODE" - if ($RequireExitCodeZero -and $LASTEXITCODE -ne 0) { - Write-Error (Get-LocString -Key PSLIB_Process0ExitedWithCode1 -ArgumentList ([System.IO.Path]::GetFileName($FileName)), $LASTEXITCODE) - } - } - finally { - if ($originalEncoding) { - [System.Console]::OutputEncoding = $originalEncoding - } - - if ($isPushed) { - Pop-Location - } - - Trace-LeavingInvocation $MyInvocation - } -} - -<# -.SYNOPSIS -Executes an external program as a child process. - -.DESCRIPTION -Executes an external program and waits for the process to exit. - -After calling this command, the exit code of the process can be retrieved from the variable $LASTEXITCODE or from the pipe. - -.PARAMETER FileName -File name (path) of the program to execute. - -.PARAMETER Arguments -Arguments to pass to the program. - -.PARAMETER StdOutPath -Path to a file to write the stdout of the process to. - -.PARAMETER StdErrPath -Path to a file to write the stderr of the process to. - -.PARAMETER RequireExitCodeZero -Indicates whether to write an error to the error pipeline if the exit code is not zero. - -.OUTPUTS -Exit code of the invoked process. Also available through the $LASTEXITCODE. - -.NOTES -To change output encoding, redirect stdout to file and then read the file with the desired encoding. -#> -function Invoke-Process { - [CmdletBinding()] - param( - [ValidatePattern('^[^\r\n]*$')] - [Parameter(Mandatory = $true)] - [string]$FileName, - [ValidatePattern('^[^\r\n]*$')] - [Parameter()] - [string]$Arguments, - [string]$WorkingDirectory, - [string]$StdOutPath, - [string]$StdErrPath, - [switch]$RequireExitCodeZero - ) - - Trace-EnteringInvocation $MyInvocation - try { - $FileName = $FileName.Replace('"', '').Replace("'", "''") - Write-Host "##[command]""$FileName"" $Arguments" - - $processOptions = @{ - FilePath = $FileName - NoNewWindow = $true - PassThru = $true - } - if ($Arguments) { - $processOptions.Add("ArgumentList", $Arguments) - } - if ($WorkingDirectory) { - $processOptions.Add("WorkingDirectory", $WorkingDirectory) - } - if ($StdOutPath) { - $processOptions.Add("RedirectStandardOutput", $StdOutPath) - } - if ($StdErrPath) { - $processOptions.Add("RedirectStandardError", $StdErrPath) - } - - # TODO: For some reason, -Wait is not working on agent. - # Agent starts executing the System usage metrics and hangs the step forever. - $proc = Start-Process @processOptions - - # https://stackoverflow.com/a/23797762 - $null = $($proc.Handle) - $proc.WaitForExit() - - $procExitCode = $proc.ExitCode - Write-Verbose "Exit code: $procExitCode" - - if ($RequireExitCodeZero -and $procExitCode -ne 0) { - Write-Error (Get-LocString -Key PSLIB_Process0ExitedWithCode1 -ArgumentList ([System.IO.Path]::GetFileName($FileName)), $procExitCode) - } - - $global:LASTEXITCODE = $procExitCode - - return $procExitCode - } - finally { - Trace-LeavingInvocation $MyInvocation - } -} diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/TraceFunctions.ps1 b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/TraceFunctions.ps1 deleted file mode 100644 index cdbd779..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/TraceFunctions.ps1 +++ /dev/null @@ -1,139 +0,0 @@ -<# -.SYNOPSIS -Writes verbose information about the invocation being entered. - -.DESCRIPTION -Used to trace verbose information when entering a function/script. Writes an entering message followed by a short description of the invocation. Additionally each bound parameter and unbound argument is also traced. - -.PARAMETER Parameter -Wildcard pattern to control which bound parameters are traced. -#> -function Trace-EnteringInvocation { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [System.Management.Automation.InvocationInfo]$InvocationInfo, - [string[]]$Parameter = '*') - - Write-Verbose "Entering $(Get-InvocationDescription $InvocationInfo)." - $OFS = ", " - if ($InvocationInfo.BoundParameters.Count -and $Parameter.Count) { - if ($Parameter.Count -eq 1 -and $Parameter[0] -eq '*') { - # Trace all parameters. - foreach ($key in $InvocationInfo.BoundParameters.Keys) { - Write-Verbose " $($key): '$($InvocationInfo.BoundParameters[$key])'" - } - } else { - # Trace matching parameters. - foreach ($key in $InvocationInfo.BoundParameters.Keys) { - foreach ($p in $Parameter) { - if ($key -like $p) { - Write-Verbose " $($key): '$($InvocationInfo.BoundParameters[$key])'" - break - } - } - } - } - } - - # Trace all unbound arguments. - if (@($InvocationInfo.UnboundArguments).Count) { - for ($i = 0 ; $i -lt $InvocationInfo.UnboundArguments.Count ; $i++) { - Write-Verbose " args[$i]: '$($InvocationInfo.UnboundArguments[$i])'" - } - } -} - -<# -.SYNOPSIS -Writes verbose information about the invocation being left. - -.DESCRIPTION -Used to trace verbose information when leaving a function/script. Writes a leaving message followed by a short description of the invocation. -#> -function Trace-LeavingInvocation { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [System.Management.Automation.InvocationInfo]$InvocationInfo) - - Write-Verbose "Leaving $(Get-InvocationDescription $InvocationInfo)." -} - -<# -.SYNOPSIS -Writes verbose information about paths. - -.DESCRIPTION -Writes verbose information about the paths. The paths are sorted and a the common root is written only once, followed by each relative path. - -.PARAMETER PassThru -Indicates whether to return the sorted paths. -#> -function Trace-Path { - [CmdletBinding()] - param( - [string[]]$Path, - [switch]$PassThru) - - if ($Path.Count -eq 0) { - Write-Verbose "No paths." - if ($PassThru) { - $Path - } - } elseif ($Path.Count -eq 1) { - Write-Verbose "Path: $($Path[0])" - if ($PassThru) { - $Path - } - } else { - # Find the greatest common root. - $sorted = $Path | Sort-Object - $firstPath = $sorted[0].ToCharArray() - $lastPath = $sorted[-1].ToCharArray() - $commonEndIndex = 0 - $j = if ($firstPath.Length -lt $lastPath.Length) { $firstPath.Length } else { $lastPath.Length } - for ($i = 0 ; $i -lt $j ; $i++) { - if ($firstPath[$i] -eq $lastPath[$i]) { - if ($firstPath[$i] -eq '\') { - $commonEndIndex = $i - } - } else { - break - } - } - - if ($commonEndIndex -eq 0) { - # No common root. - Write-Verbose "Paths:" - foreach ($p in $sorted) { - Write-Verbose " $p" - } - } else { - Write-Verbose "Paths: $($Path[0].Substring(0, $commonEndIndex + 1))" - foreach ($p in $sorted) { - Write-Verbose " $($p.Substring($commonEndIndex + 1))" - } - } - - if ($PassThru) { - $sorted - } - } -} - -######################################## -# Private functions. -######################################## -function Get-InvocationDescription { - [CmdletBinding()] - param([System.Management.Automation.InvocationInfo]$InvocationInfo) - - if ($InvocationInfo.MyCommand.Path) { - $InvocationInfo.MyCommand.Path - } elseif ($InvocationInfo.MyCommand.Name) { - $InvocationInfo.MyCommand.Name - } else { - $InvocationInfo.MyCommand.CommandType - } -} diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/VstsTaskSdk.dll b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/VstsTaskSdk.dll deleted file mode 100644 index 54938ab05180ddaf77cf067f2501a1f97ee7e5cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25408 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

8wuJ@>HyfDZ;=w9&`lhU;c$=Kxs zVyl3=oCzOR%(5JON~b1f_qN+!&dWl_U8}fsV#Ix3YxI$et=Qqtrbj{FWE$c^p03gw zvH`3R`Cpf{D%4sIAA!^?|@!O&A+cc|Ex;y|6?0F+wS9quHW9^=zrzn|8Li++DDT^mzVoUZBHf{%(A&` z+&F2uZRr5BJJx3?7i7uTq+cCqJ8j`#b-DS*ZPDce-qXtB9+EzMH(i&wvaNu;{++#% zcYtkc-G2VJ;sr0;Hp{x<5&6O@^OK@zp)En2H5kd8W?t#(tXU5Z@fTX(n%L1~H8J*xd0XA~tGY9FO!V{GAFRD#G3e@uMn47pstt`F{7#1- zWi49pHQ5e*sle1z<+k#Y0!F=g&RZiY9=|6+d&$Bv^Oy%)?#h}hI33WlP7MZ5srI`8 zDs#%WStH@>)5B>?n&RoQiFMb_FJ&u2u-$bQcf-@rv^-_J?e9(`T1x!=l656Xj>}^1^N?Y zEH15`x&O7D>6gB*9@02r$cOH;!{#ET_rTm5&V**TKN>0b;r!$&7 zOAU12dChrl?rV^B`RJpSS7)tK4q5!9fO%flyo3_*&h4K1ZC-4k&b8r6MwZ@JiaM)~ z+|?P{_g#wv|hI+^_}a5)~dOiu6HO_wO15Ta=@hEB<@82bDt@uz~NpDL_$1jV})P zncLo2a%j)IPX-%V{8`iIS(O(a!3qym6&}C}>;5B#_pd(&`QvX6gCiq|-yB-m+1Z-g zcv}0Id)j(=vrIALE_+?~S9_fqA;=PPxXd8|_?4dG}ryA~vq&3``QwMp3z+P|*{XsQBd)PR}pjq?*aeSuZZ! zRSve}Uh-LgC!#g_l=p1Kmv>pm+V?CrcJmz8xH#o_dB(OIMI$FLj3nmd>fAl(#7&wb zecG^fjm+4KckiE270=kN8Ii*_)Ob2PZJaG-S^BkBlM>gxoZV8x3k$bA^TtGY z@7tt9+x9W*H5SEK2-lwObj0@R961rZVGYTEzo-!}zCMc?y&%O1 zG7Ij;b)@+gJzBYZ`i5}x{cW+|S6l2U74)C1u;}1diVEIM-19nj@yVoFe~6!l z%wh{Uc5cN6D|N&D&*mS#H~7>BFH=p|<6jsSG3Qc?atAbZls%Lx^Y^#hYTvSsYPg&f zaAuI>jn3V3Xt^=l;%HyeiyFg8QdckPyCeg8Z=(>)u$VSSVt;j6wbe_ zw6|WRHhZ|_nuYUYyjeTqB4-qOB%91~SKi&KH{s{*;caH?8)~j|wf1CA91}&4Q(dOr ze1vf_*<|*suxPoIQ~R85&NbT^@0+1{t#L-gHs8oM&1 | - ForEach-Object { - if (`$_ -is [System.Management.Automation.ErrorRecord]) { - Write-Verbose `$_.Exception.Message - } else { - ,`$_ - } - } -} -"@ -. ([scriptblock]::Create($scriptText)) 2>&1 3>&1 4>&1 5>&1 | Out-Default - -# Create Invoke-VstsTaskScript in a special way so it is not bound to the module. -# Otherwise calling the task script block would run within the module context. -# -# An alternative way to solve the problem is to close the script block (i.e. closure). -# However, that introduces a different problem. Closed script blocks are created within -# a dynamic module. Each module gets it's own session state separate from the global -# session state. When running in a regular script context, Import-Module calls import -# the target module into the global session state. When running in a module context, -# Import-Module calls import the target module into the caller module's session state. -# -# The goal of a task may include executing ad-hoc scripts. Therefore, task scripts -# should run in regular script context. The end user specifying an ad-hoc script expects -# the module import rules to be consistent with the default behavior (i.e. imported -# into the global session state). -$null = New-Item -Force -Path "function:\global:Invoke-VstsTaskScript" -Value ([scriptblock]::Create(@' - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [scriptblock]$ScriptBlock) - - try { - $global:ErrorActionPreference = 'Stop' - - # Initialize the environment. - $vstsModule = Get-Module -Name VstsTaskSdk - Write-Verbose "$($vstsModule.Name) $($vstsModule.Version) commit $($vstsModule.PrivateData.PSData.CommitHash)" 4>&1 | Out-Default - & $vstsModule Initialize-Inputs 4>&1 | Out-Default - - # Remove the local variable before calling the user's script. - Remove-Variable -Name vstsModule - - # Call the user's script. - $ScriptBlock | - ForEach-Object { - # Remove the scriptblock variable before calling it. - Remove-Variable -Name ScriptBlock - & $_ 2>&1 3>&1 4>&1 5>&1 | Out-Default - } - } catch [VstsTaskSdk.TerminationException] { - # Special internal exception type to control the flow. Not currently intended - # for public usage and subject to change. - $global:__vstsNoOverrideVerbose = '' - Write-Verbose "Task script terminated." 4>&1 | Out-Default - } catch { - $global:__vstsNoOverrideVerbose = '' - Write-Verbose "Caught exception from task script." 4>&1 | Out-Default - $_ | Out-Default - Write-Host "##vso[task.complete result=Failed]" - } -'@)) diff --git a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/lib.json b/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/lib.json deleted file mode 100644 index 0cde160..0000000 --- a/bc-tools-extension/Build-ALPackage/ps_modules/VstsTaskSdk/lib.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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", - "PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Enumerating subdirectories failed for path: '{0}'", - "PSLIB_FileNotFound0": "File not found: '{0}'", - "PSLIB_Input0": "'{0}' input", - "PSLIB_InvalidPattern0": "Invalid pattern: '{0}'", - "PSLIB_LeafPathNotFound0": "Leaf path not found: '{0}'", - "PSLIB_PathLengthNotReturnedFor0": "Path normalization/expansion failed. The path length was not returned by the Kernel32 subsystem for: '{0}'", - "PSLIB_PathNotFound0": "Path not found: '{0}'", - "PSLIB_Process0ExitedWithCode1": "Process '{0}' exited with code '{1}'.", - "PSLIB_Required0": "Required: {0}", - "PSLIB_StringFormatFailed": "String format failed.", - "PSLIB_StringResourceKeyNotFound0": "String resource key not found: '{0}'", - "PSLIB_TaskVariable0": "'{0}' task variable" - } -} diff --git a/bc-tools-extension/Build-ALPackage/task.json b/bc-tools-extension/Build-ALPackage/task.json index f3b2479..2d92799 100644 --- a/bc-tools-extension/Build-ALPackage/task.json +++ b/bc-tools-extension/Build-ALPackage/task.json @@ -48,7 +48,7 @@ { "name": "ALEXEPathFolder", "type": "string", - "label": "AL.EXE Path", + "label": "ALC Path", "defaultValue": "", "required": true, "helpMarkDown": "Path to the ALC.EXE folder, one folder above. (The next folder down is 'win32'.)" @@ -60,9 +60,6 @@ }, "Node20_1": { "target": "function_Build-ALPackage.js" - }, - "PowerShell3": { - "target": "wrapper_Build-ALPackage.ps1" } } } diff --git a/bc-tools-extension/Build-ALPackage/wrapper_Build-ALPackage.ps1 b/bc-tools-extension/Build-ALPackage/wrapper_Build-ALPackage.ps1 deleted file mode 100644 index a26e05a..0000000 --- a/bc-tools-extension/Build-ALPackage/wrapper_Build-ALPackage.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -. "./function_Build-ALPackage.ps1" - -$local_ProjectPath = Get-VstsInput -Name 'ProjectPath' -$local_OutAppFolder = Get-VstsInput -Name 'OutAppFolder' -$local_PackageCachePath = Get-VstsInput -Name 'PackageCachePath' -$local_ALEXEPathFolder = Get-VstsInput -Name 'ALEXEPathFolder' -$local_EntireAppName = Get-VstsInput -Name 'EntireAppName' - -Write-Host "Building AL Package:" -Write-Host " ProjectPath = $local_ProjectPath" -Write-Host " OutAppFolder = $local_OutAppFolder" -Write-Host " PackageCachePath = $local_PackageCachePath" -Write-Host " ALEXEPathFolder = $local_ALEXEPathFolder" -Write-Host " EntireAppName = $local_EntireAppName" - -Build-ALPackage -EntireAppName $local_EntireAppName -BaseProjectDirectory $local_ProjectPath -PackagesDirectory $local_PackageCachePath -OutputDirectory $local_OutAppFolder -ALEXEPath $local_ALEXEPathFolder diff --git a/bc-tools-extension/Get-BCDependencies/Tests/Unit/Tests.Add-BaseDependenciesIfMissing.ps1 b/bc-tools-extension/Get-BCDependencies/Tests/Unit/Tests.Add-BaseDependenciesIfMissing.ps1 deleted file mode 100644 index 589d531..0000000 --- a/bc-tools-extension/Get-BCDependencies/Tests/Unit/Tests.Add-BaseDependenciesIfMissing.ps1 +++ /dev/null @@ -1,59 +0,0 @@ -Describe "Add-BaseDependenciesIfMissing" { - - BeforeAll { - . "$PSScriptRoot/../../function_Add-BaseDependenciesIfMissing.ps1" - } - - Context "When no dependencies exist" { - It "Adds all base dependencies" { - $appJson = [PSCustomObject]@{ - id = "test" - name = "Test App" - publisher = "Test Publisher" - dependencies = @() - } - - $result = Add-BaseDependenciesIfMissing -AppJson $appJson - - # There are 6 hardcoded dependencies in the function - $result.dependencies.Count | Should -Be 6 - } - } - - Context "When some dependencies already exist" { - It "Does not add duplicates" { - $appJson = [PSCustomObject]@{ - id = "test" - name = "Test App" - publisher = "Test Publisher" - dependencies = @( - [PSCustomObject]@{ - id = "437dbf0e-84ff-417a-965d-ed2bb9650972" - name = "Base Application" - publisher = "Microsoft" - version = "" - } - ) - } - - $result = Add-BaseDependenciesIfMissing -AppJson $appJson - - # Should add 5 new dependencies (1 already exists) - $result.dependencies.Count | Should -Be 6 - } - } - - Context "When dependencies is $null or missing" { - It "Initializes the dependencies list and adds base dependencies" { - $appJson = [PSCustomObject]@{ - id = "test" - name = "Test App" - publisher = "Test Publisher" - } - - $result = Add-BaseDependenciesIfMissing -AppJson $appJson - - $result.dependencies.Count | Should -Be 6 - } - } -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/function_Add-BaseDependenciesIfMissing.ps1 b/bc-tools-extension/Get-BCDependencies/function_Add-BaseDependenciesIfMissing.ps1 deleted file mode 100644 index b76146a..0000000 --- a/bc-tools-extension/Get-BCDependencies/function_Add-BaseDependenciesIfMissing.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -<# -.SYNOPSIS - An internal tool used to add to the app.json for missing dependencies -.DESCRIPTION - Conditionally adds (normally) missing required packages for compiling. These include system, application, etc. -.PARAMETER AppJSON - This expects the previously parsed app.json file. Since this is an internal tool, it is on the user to ensure that it returns what is expected. -.OUTPUTS - The same AppJSON with the dependencies added. -.NOTES - Author : James McCullough - Company : Evergrowth Consulting - Version : 1.0.0 - Created : 2025-05-21 - Purpose : DevOps-compatible AL extension internal tool to add dependencies -#> -function Add-BaseDependenciesIfMissing { - param( - [Parameter(Mandatory)] - [PSCustomObject]$AppJSON - ) - ######################################################################################################################################## - # These items are usually mandatory, and don't always appear in an app.json dependencies; load them if missing - ######################################################################################################################################## - $baseDependencies = @( - [PSCustomObject]@{ - id = "63ca2fa4-4f03-4f2b-a480-172fef340d3f" - name = "System Application" - publisher = "Microsoft" - version = "" - }, - [PSCustomObject]@{ - id = "f3552374-a1f2-4356-848e-196002525837" - name = "Business Foundation" - publisher = "Microsoft" - version = "" - }, - [PSCustomObject]@{ - id = "437dbf0e-84ff-417a-965d-ed2bb9650972" - name = "Base Application" - publisher = "Microsoft" - version = "" - }, - [PSCustomObject]@{ - id = "6f2c034f-5ebe-4eae-b34c-90a0d4e87687" - name = "_Exclude_Business_Events_" - publisher = "Microsoft" - version = "" - }, - [PSCustomObject]@{ - id = "8874ed3a-0643-4247-9ced-7a7002f7135d" - name = "System" - publisher = "Microsoft" - version = "" - }, - [PSCustomObject]@{ - id = "00000000-0000-0000-0000-000000000000" - name = "Application" - publisher = "Microsoft" - version = "" - } - ) - - if (-not $AppJSON.PSObject.Properties['dependencies']) { - $AppJSON | Add-Member -MemberType NoteProperty -Name dependencies -Value @() - } - - # Get existing dependency IDs once, cleanly - $existingIds = $AppJSON.dependencies | Where-Object { $_.id } | ForEach-Object { $_.id } - - foreach ($baseDependency in $baseDependencies) { - if ($baseDependency.id -notin $existingIds) { - Write-Host "Adding dependency $($baseDependency.name) from $($baseDependency.publisher) to dependencies with guid $($baseDependency.id)" - $AppJSON.dependencies += $baseDependency - } - } - - return $AppJSON -} diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 deleted file mode 100644 index 4389769..0000000 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.ps1 +++ /dev/null @@ -1,229 +0,0 @@ -# this script is used to extract the dependencies from the app.json file and download them automatically -# -# required data: -# - app.json -# - Azure AD Client ID for development user -# - Azure AD Client Secret for development user - -<# -.SYNOPSIS - Gets the dependencies required for compiling a Business Central extension based on the extension's app.json -.DESCRIPTION - Parses the app.json file in an extension and uses that information with the parameters to collect the packages required for compilation into an output directory. - - Requirements: - - an application registration in Azure Entra, with the client id and client secret; it requires "app_access" and "API.ReadWrite.All" from the Business Central APIs - - the application MUST ALSO be registered in Business Central as an Entra application with "EXTEN. MGT. - ADMIN" (at minimum) - - If the download returns a 500 despite valid authentication, the problem is likely either: - - missing Business Central app registration permissions, or - - incorrect/missing API permissions in Entra ID -.PARAMETER TenantId - The Azure tenant id of the entity that is _compiling_ the extension, not the client's tenant id -.PARAMETER EnvironmentName - The environment name of the Business Central environment being used to compile the extension, not the client environment name. Default: 'sandbox' -.PARAMETER ClientId - The client id that has been set up for this process to allow this script to log in to Business Central's API -.PARAMETER ClientSecret - The client secret that has been set up for this process to allow this script to log in to Business Central's API -.PARAMETER PathToAppJson - The file path to the app.json file. Default "./.app.json" -.PARAMETER PathToPackagesDirectory - The folder path to the output directory for packages. Default "./.alpackages" -.PARAMETER TestLoginOnly - Allows the script to test the login credentials and advise of results, without downloading packages -.NOTES - Author : James McCullough - Company : Evergrowth Consulting - Version : 1.0.0 - Created : 2025-05-21 - Purpose : DevOps-compatible symbol dependency resolution for AL extension builds -#> -function Get-BCDependencies { - param( - [Parameter(Mandatory)] - [String]$TenantId, - - [Parameter()] - [String]$EnvironmentName, - - [Parameter(Mandatory)] - [String]$ClientId, - - [Parameter(Mandatory)] - [String]$ClientSecret, - - [Parameter()] - [String]$PathToAppJson, - - [Parameter()] - [String]$PathToPackagesDirectory, - - [Parameter()] - [Switch]$TestLoginOnly, - - [Parameter()] - [Switch]$SkipDefaultDependencies - - ) - - ######################################################################################################################################## - # Variable setups, assignments and confirmations - ######################################################################################################################################## - - # determine if the helper function exists, if not show a warning - if (-not (Get-Command -Name "Add-BaseDependenciesIfMissing" -CommandType Function -ErrorAction SilentlyContinue) -and -not $SkipDefaultDependencies) { - Write-Warning "The function Add-BaseDependenciesIfMissing is not loaded. It is highly recommended that the user use this function" - Write-Warning "to add dependencies that are normally missing from build scenarios." - $SkipDefaultDependencies = $true - } - - # Test the path first to ensure that it can find and parse app.json - - if (-not $PathToAppJson) { - $appJSONFile = ".\app.json" - } - else { - $appJSONFile = Join-Path -Path $PathToAppJson -ChildPath "app.json" - } - - Write-Verbose "appJSONFile: $appJSONFile" - - if (-not (Test-Path -Path $appJSONFile)) { - throw "app.json not found at '$appJSONFile'; exiting..." - } else { - Write-Verbose "confirmed existence of app.json file at $appJSONFile" - } - - Write-Verbose "starting parsing app.json" - - $appJSON = Get-Content -Raw $appJSONFile | ConvertFrom-Json - - Write-Verbose "converted app.json; confirming now" - - Write-Host "Found the following before validation routine:" - $appJsonExists = if ($appJSON) {"true"} else {"false"} - Write-Host (" {0,-30} = {1}" -f "app.json exists", $appJsonExists) - Write-Host (" {0,-30} = {1}" -f "app.id", $appJSON.id) - Write-Host (" {0,-30} = {1}" -f "app.name", $appJSON.name) - Write-Host (" {0,-30} = {1}" -f "app.publisher", $appJSON.publisher) - - if (-not $appJSON -or -not $appJSON.id -or -not $appJSON.name -or -not $appJSON.publisher) { - throw "app.json found at '$appJSONFile' does not appear to be valid; exiting..." - } - - Write-Host "app.json loaded from '$appJSONFile' with name $($appJSON.name) published by $($appJSON.publisher)" - - if (-not $SkipDefaultDependencies) { $appJSON = Add-BaseDependenciesIfMissing -AppJSON $appJSON } - - Write-Host "Collecting $(($appJSON.dependencies | Measure-Object).Count) dependencies" - - # Test the path to the packages output directory (if specified, default if not), create it if it doesn't exist - - if (-not $PathToPackagesDirectory) { - $targetPackageDirectory = Join-Path -Path $PathToAppJson -ChildPath ".alpackages" - } - else { - $targetPackageDirectory = $PathToPackagesDirectory - } - - if (-not (Test-Path -Path $targetPackageDirectory)) { - New-Item -ItemType Directory -Path $targetPackageDirectory - Write-Host "Created package directory at: '$($targetPackageDirectory)'" - } - else { - Write-Host "Found output directory at: '$($targetPackageDirectory)'" - } - - # set environment name to default if it is missing - - if (-not $EnvironmentName) { - $EnvironmentName = "sandbox" - } - - # variable assignments for setup; note: variables with '%_foo_%' are replaced internally within context of this script, not by an external tool - - $authurl = "https://login.microsoftonline.com/$($TenantId)/oauth2/v2.0/token" - $scope = "https://api.businesscentral.dynamics.com/.default" - - $publisherToken = "%_PUBLISHER_%" - $appNameToken = "%_APP_NAME_%" - $versionToken = "%_VERSION_TEXT_%" - $appIDToken = "%_APP_ID_%" - - $originSite = "https://api.businesscentral.dynamics.com/v2.0/$($EnvironmentName)/dev/packages?publisher=$($publisherToken)&appName=$($appNameToken)&versionText=$($versionToken)&appId=$($appIDToken)" - - $authBody = "grant_type=client_credentials&client_id=$ClientId&client_secret=$ClientSecret&scope=$scope" - - ######################################################################################################################################## - # Action section starts here - ######################################################################################################################################## - - # First things first, kill the progress bar because it interferes with logs - $ProgressPreference = 'SilentlyContinue' - - # get token for login - try { - - $tokenResponse = Invoke-RestMethod -Method Post -Uri $authurl -Body $authBody -ContentType "application/x-www-form-urlencoded" - - # Extract the token from the response - $token = $tokenResponse.access_token - - $authSuccess = $true - "Authenticated correctly; moving on" - } - catch { - Write-Host "Error: $($_.Exception.Message)" - $authSuccess = $false - "ERROR - Authentication failed" - } - - if ($TestLoginOnly) { - if ($authSuccess) { - Write-Host "TestLoginOnly specified and was successful; exiting normally..." - return - } - else { - Write-Host "TestLoginOnly specified and failed; exiting with error..." - throw "Authentication failed" - } - } - else { - if (-not $authSuccess) { - throw "Authentication failed; exiting" - } - } - - # these are the dependencies listed; we still need to grab the base system and the application itself, listed by "platform" and "application" in app.json - foreach ($dependency in $appJSON.dependencies) { - $siteID = $originSite.Replace($publisherToken, $dependency.publisher).Replace($appNameToken, [System.Web.HttpUtility]::UrlEncode($dependency.name)).Replace($versionToken, $dependency.version).Replace($appIDToken, $dependency.id) - try { - $response = Invoke-WebRequest $siteID -Headers @{"Authorization" = "Bearer " + $token } - } - catch { - Write-Error "Failed to download symbol for $AppName $Version ($Publisher). $($_.Exception.Message)" - throw - } - $responseHeaderCD = $response.Headers['Content-Disposition'] - $disposition = [System.Net.Mime.ContentDisposition]::new($responseHeaderCD) - [IO.File]::WriteAllBytes($targetPackageDirectory + "\" + $disposition.FileName, $response.Content) - - $appName = if (![string]::IsNullOrEmpty($dependency.appName)) { $dependency.appName } else { $dependency.name } - Write-Host "Dependency $appName ($($dependency.id)) downloaded" - } - - if ($IsLinux) { - Write-Host "Executing chmod to allow access for all of the extracted files" - chmod -R 644 $$TopExtractedFolder - } - - # Debug section+ - Write-Debug "########################################################################################################################################" - Write-Debug "Enumerating filesystem from '$targetPackageDirectory'" - Write-Debug "########################################################################################################################################" - Write-Debug "" - Get-ChildItem -Path "$targetPackageDirectory" -Force -Recurse | ForEach-Object { Write-Debug $_.FullName } - Write-Debug "" - Write-Debug "########################################################################################################################################" -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/FindFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/FindFunctions.ps1 deleted file mode 100644 index c0278ea..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/FindFunctions.ps1 +++ /dev/null @@ -1,728 +0,0 @@ -<# -.SYNOPSIS -Finds files using match patterns. - -.DESCRIPTION -Determines the find root from a list of patterns. Performs the find and then applies the glob patterns. Supports interleaved exclude patterns. Unrooted patterns are rooted using defaultRoot, unless matchOptions.matchBase is specified and the pattern is a basename only. For matchBase cases, the defaultRoot is used as the find root. - -.PARAMETER DefaultRoot -Default path to root unrooted patterns. Falls back to System.DefaultWorkingDirectory or current location. - -.PARAMETER Pattern -Patterns to apply. Supports interleaved exclude patterns. - -.PARAMETER FindOptions -When the FindOptions parameter is not specified, defaults to (New-VstsFindOptions -FollowSymbolicLinksTrue). Following soft links is generally appropriate unless deleting files. - -.PARAMETER MatchOptions -When the MatchOptions parameter is not specified, defaults to (New-VstsMatchOptions -Dot -NoBrace -NoCase). -#> -function Find-Match { - [CmdletBinding()] - param( - [Parameter()] - [string]$DefaultRoot, - [Parameter()] - [string[]]$Pattern, - $FindOptions, - $MatchOptions) - - Trace-EnteringInvocation $MyInvocation -Parameter None - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Apply defaults for parameters and trace. - if (!$DefaultRoot) { - $DefaultRoot = Get-TaskVariable -Name 'System.DefaultWorkingDirectory' -Default (Get-Location).Path - } - - Write-Verbose "DefaultRoot: '$DefaultRoot'" - if (!$FindOptions) { - $FindOptions = New-FindOptions -FollowSpecifiedSymbolicLink -FollowSymbolicLinks - } - - Trace-FindOptions -Options $FindOptions - if (!$MatchOptions) { - $MatchOptions = New-MatchOptions -Dot -NoBrace -NoCase - } - - Trace-MatchOptions -Options $MatchOptions - Add-Type -LiteralPath $PSScriptRoot\Minimatch.dll - - # Normalize slashes for root dir. - $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot - - $results = @{ } - $originalMatchOptions = $MatchOptions - foreach ($pat in $Pattern) { - Write-Verbose "Pattern: '$pat'" - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue - } - - # Clone match options. - $MatchOptions = Copy-MatchOptions -Options $originalMatchOptions - - # Skip comments. - if (!$MatchOptions.NoComment -and $pat.StartsWith('#')) { - Write-Verbose 'Skipping comment.' - continue - } - - # Set NoComment. Brace expansion could result in a leading '#'. - $MatchOptions.NoComment = $true - - # Determine whether pattern is include or exclude. - $negateCount = 0 - if (!$MatchOptions.NoNegate) { - while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') { - $negateCount++ - } - - $pat = $pat.Substring($negateCount) # trim leading '!' - if ($negateCount) { - Write-Verbose "Trimmed leading '!'. Pattern: '$pat'" - } - } - - $isIncludePattern = $negateCount -eq 0 -or - ($negateCount % 2 -eq 0 -and !$MatchOptions.FlipNegate) -or - ($negateCount % 2 -eq 1 -and $MatchOptions.FlipNegate) - - # Set NoNegate. Brace expansion could result in a leading '!'. - $MatchOptions.NoNegate = $true - $MatchOptions.FlipNegate = $false - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue - } - - # Expand braces - required to accurately interpret findPath. - $expanded = $null - $preExpanded = $pat - if ($MatchOptions.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 $MatchOptions)) - } - - # Set NoBrace. - $MatchOptions.NoBrace = $true - - foreach ($pat in $expanded) { - if ($pat -ne $preExpanded) { - Write-Verbose "Pattern: '$pat'" - } - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose "Skipping empty pattern." - continue - } - - if ($isIncludePattern) { - # Determine the findPath. - $findInfo = Get-FindInfoFromPattern -DefaultRoot $DefaultRoot -Pattern $pat -MatchOptions $MatchOptions - $findPath = $findInfo.FindPath - Write-Verbose "FindPath: '$findPath'" - - if (!$findPath) { - Write-Verbose "Skipping empty path." - continue - } - - # Perform the find. - Write-Verbose "StatOnly: '$($findInfo.StatOnly)'" - [string[]]$findResults = @( ) - if ($findInfo.StatOnly) { - # Simply stat the path - all path segments were used to build the path. - if ((Test-Path -LiteralPath $findPath)) { - $findResults += $findPath - } - } else { - $findResults = @( Get-FindResult -Path $findPath -Options $FindOptions ) - } - - Write-Verbose "Found $($findResults.Count) paths." - - # Apply the pattern. - Write-Verbose "Applying include pattern." - if ($findInfo.AdjustedPattern -ne $pat) { - Write-Verbose "AdjustedPattern: '$($findInfo.AdjustedPattern)'" - $pat = $findInfo.AdjustedPattern - } - - $matchResults = [Minimatch.Minimatcher]::Filter( - $findResults, - $pat, - (ConvertTo-MinimatchOptions -Options $MatchOptions)) - - # Union the results. - $matchCount = 0 - foreach ($matchResult in $matchResults) { - $matchCount++ - $results[$matchResult.ToUpperInvariant()] = $matchResult - } - - Write-Verbose "$matchCount matches" - } else { - # Check if basename only and MatchBase=true. - if ($MatchOptions.MatchBase -and - !(Test-Rooted -Path $pat) -and - ($pat -replace '\\', '/').IndexOf('/') -lt 0) { - - # Do not root the pattern. - Write-Verbose "MatchBase and basename only." - } else { - # Root the exclude pattern. - $pat = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $pat - Write-Verbose "After Get-RootedPattern, pattern: '$pat'" - } - - # Apply the pattern. - Write-Verbose 'Applying exclude pattern.' - $matchResults = [Minimatch.Minimatcher]::Filter( - [string[]]$results.Values, - $pat, - (ConvertTo-MinimatchOptions -Options $MatchOptions)) - - # Subtract the results. - $matchCount = 0 - foreach ($matchResult in $matchResults) { - $matchCount++ - $results.Remove($matchResult.ToUpperInvariant()) - } - - Write-Verbose "$matchCount matches" - } - } - } - - $finalResult = @( $results.Values | Sort-Object ) - Write-Verbose "$($finalResult.Count) final results" - return $finalResult - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Creates FindOptions for use with Find-VstsMatch. - -.DESCRIPTION -Creates FindOptions for use with Find-VstsMatch. Contains switches to control whether to follow symlinks. - -.PARAMETER FollowSpecifiedSymbolicLink -Indicates whether to traverse descendants if the specified path is a symbolic link directory. Does not cause nested symbolic link directories to be traversed. - -.PARAMETER FollowSymbolicLinks -Indicates whether to traverse descendants of symbolic link directories. -#> -function New-FindOptions { - [CmdletBinding()] - param( - [switch]$FollowSpecifiedSymbolicLink, - [switch]$FollowSymbolicLinks) - - return New-Object psobject -Property @{ - FollowSpecifiedSymbolicLink = $FollowSpecifiedSymbolicLink.IsPresent - FollowSymbolicLinks = $FollowSymbolicLinks.IsPresent - } -} - -<# -.SYNOPSIS -Creates MatchOptions for use with Find-VstsMatch and Select-VstsMatch. - -.DESCRIPTION -Creates MatchOptions for use with Find-VstsMatch and Select-VstsMatch. Contains switches to control which pattern matching options are applied. -#> -function New-MatchOptions { - [CmdletBinding()] - param( - [switch]$Dot, - [switch]$FlipNegate, - [switch]$MatchBase, - [switch]$NoBrace, - [switch]$NoCase, - [switch]$NoComment, - [switch]$NoExt, - [switch]$NoGlobStar, - [switch]$NoNegate, - [switch]$NoNull) - - return New-Object psobject -Property @{ - Dot = $Dot.IsPresent - FlipNegate = $FlipNegate.IsPresent - MatchBase = $MatchBase.IsPresent - NoBrace = $NoBrace.IsPresent - NoCase = $NoCase.IsPresent - NoComment = $NoComment.IsPresent - NoExt = $NoExt.IsPresent - NoGlobStar = $NoGlobStar.IsPresent - NoNegate = $NoNegate.IsPresent - NoNull = $NoNull.IsPresent - } -} - -<# -.SYNOPSIS -Applies match patterns against a list of files. - -.DESCRIPTION -Applies match patterns to a list of paths. Supports interleaved exclude patterns. - -.PARAMETER ItemPath -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). -#> -function Select-Match { - [CmdletBinding()] - param( - [Parameter()] - [string[]]$ItemPath, - [Parameter()] - [string[]]$Pattern, - [Parameter()] - [string]$PatternRoot, - $Options) - - - Trace-EnteringInvocation $MyInvocation -Parameter None - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - if (!$Options) { - $Options = New-MatchOptions -Dot -NoBrace -NoCase - } - - Trace-MatchOptions -Options $Options - Add-Type -LiteralPath $PSScriptRoot\Minimatch.dll - - # Hashtable to keep track of matches. - $map = @{ } - - $originalOptions = $Options - foreach ($pat in $Pattern) { - Write-Verbose "Pattern: '$pat'" - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue - } - - # Clone match options. - $Options = Copy-MatchOptions -Options $originalOptions - - # Skip comments. - if (!$Options.NoComment -and $pat.StartsWith('#')) { - Write-Verbose 'Skipping comment.' - continue - } - - # Set NoComment. Brace expansion could result in a leading '#'. - $Options.NoComment = $true - - # Determine whether pattern is include or exclude. - $negateCount = 0 - if (!$Options.NoNegate) { - while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') { - $negateCount++ - } - - $pat = $pat.Substring($negateCount) # trim leading '!' - if ($negateCount) { - Write-Verbose "Trimmed leading '!'. Pattern: '$pat'" - } - } - - $isIncludePattern = $negateCount -eq 0 -or - ($negateCount % 2 -eq 0 -and !$Options.FlipNegate) -or - ($negateCount % 2 -eq 1 -and $Options.FlipNegate) - - # Set NoNegate. Brace expansion could result in a leading '!'. - $Options.NoNegate = $true - $Options.FlipNegate = $false - - # 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)) - } - - # Set NoBrace. - $Options.NoBrace = $true - - foreach ($pat in $expanded) { - if ($pat -ne $preExpanded) { - Write-Verbose "Pattern: '$pat'" - } - - # 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)) { - - # Root the include pattern. - $pat = Get-RootedPattern -DefaultRoot $PatternRoot -Pattern $pat - Write-Verbose "After Get-RootedPattern, pattern: '$pat'" - } - - 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" - } - } - } - - # return a filtered version of the original list (preserves order and prevents duplication) - $result = $ItemPath | Where-Object { $map[$_] } - Write-Verbose "$($result.Count) final results" - $result - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -################################################################################ -# Private functions. -################################################################################ - -function Copy-MatchOptions { - [CmdletBinding()] - param($Options) - - return New-Object psobject -Property @{ - Dot = $Options.Dot -eq $true - FlipNegate = $Options.FlipNegate -eq $true - MatchBase = $Options.MatchBase -eq $true - NoBrace = $Options.NoBrace -eq $true - NoCase = $Options.NoCase -eq $true - NoComment = $Options.NoComment -eq $true - NoExt = $Options.NoExt -eq $true - NoGlobStar = $Options.NoGlobStar -eq $true - NoNegate = $Options.NoNegate -eq $true - NoNull = $Options.NoNull -eq $true - } -} - -function ConvertTo-MinimatchOptions { - [CmdletBinding()] - param($Options) - - $opt = New-Object Minimatch.Options - $opt.AllowWindowsPaths = $true - $opt.Dot = $Options.Dot -eq $true - $opt.FlipNegate = $Options.FlipNegate -eq $true - $opt.MatchBase = $Options.MatchBase -eq $true - $opt.NoBrace = $Options.NoBrace -eq $true - $opt.NoCase = $Options.NoCase -eq $true - $opt.NoComment = $Options.NoComment -eq $true - $opt.NoExt = $Options.NoExt -eq $true - $opt.NoGlobStar = $Options.NoGlobStar -eq $true - $opt.NoNegate = $Options.NoNegate -eq $true - $opt.NoNull = $Options.NoNull -eq $true - return $opt -} - -function ConvertTo-NormalizedSeparators { - [CmdletBinding()] - param([string]$Path) - - # Convert slashes. - $Path = "$Path".Replace('/', '\') - - # Remove redundant slashes. - $isUnc = $Path -match '^\\\\+[^\\]' - $Path = $Path -replace '\\\\+', '\' - if ($isUnc) { - $Path = '\' + $Path - } - - return $Path -} - -function Get-FindInfoFromPattern { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$DefaultRoot, - [Parameter(Mandatory = $true)] - [string]$Pattern, - [Parameter(Mandatory = $true)] - $MatchOptions) - - if (!$MatchOptions.NoBrace) { - throw "Get-FindInfoFromPattern expected MatchOptions.NoBrace to be true." - } - - # For the sake of determining the find path, pretend NoCase=false. - $MatchOptions = Copy-MatchOptions -Options $MatchOptions - $MatchOptions.NoCase = $false - - # Check if basename only and MatchBase=true - if ($MatchOptions.MatchBase -and - !(Test-Rooted -Path $Pattern) -and - ($Pattern -replace '\\', '/').IndexOf('/') -lt 0) { - - return New-Object psobject -Property @{ - AdjustedPattern = $Pattern - FindPath = $DefaultRoot - StatOnly = $false - } - } - - # The technique applied by this function is to use the information on the Minimatch object determine - # the findPath. Minimatch breaks the pattern into path segments, and exposes information about which - # segments are literal vs patterns. - # - # Note, the technique currently imposes a limitation for drive-relative paths with a glob in the - # first segment, e.g. C:hello*/world. It's feasible to overcome this limitation, but is left unsolved - # for now. - $minimatchObj = New-Object Minimatch.Minimatcher($Pattern, (ConvertTo-MinimatchOptions -Options $MatchOptions)) - - # The "set" field is a two-dimensional enumerable of parsed path segment info. The outer enumerable should only - # contain one item, otherwise something went wrong. Brace expansion can result in multiple items in the outer - # enumerable, but that should be turned off by the time this function is reached. - # - # Note, "set" is a private field in the .NET implementation but is documented as a feature in the nodejs - # implementation. The .NET implementation is a port and is by a different author. - $setFieldInfo = $minimatchObj.GetType().GetField('set', 'Instance,NonPublic') - [object[]]$set = $setFieldInfo.GetValue($minimatchObj) - if ($set.Count -ne 1) { - throw "Get-FindInfoFromPattern expected Minimatch.Minimatcher(...).set.Count to be 1. Actual: '$($set.Count)'" - } - - [string[]]$literalSegments = @( ) - [object[]]$parsedSegments = $set[0] - foreach ($parsedSegment in $parsedSegments) { - if ($parsedSegment.GetType().Name -eq 'LiteralItem') { - # The item is a LiteralItem when the original input for the path segment does not contain any - # unescaped glob characters. - $literalSegments += $parsedSegment.Source; - continue - } - - break; - } - - # Join the literal segments back together. Minimatch converts '\' to '/' on Windows, then squashes - # consequetive slashes, and finally splits on slash. This means that UNC format is lost, but can - # be detected from the original pattern. - $joinedSegments = [string]::Join('/', $literalSegments) - if ($joinedSegments -and ($Pattern -replace '\\', '/').StartsWith('//')) { - $joinedSegments = '/' + $joinedSegments # restore UNC format - } - - # Determine the find path. - $findPath = '' - if ((Test-Rooted -Path $Pattern)) { # The pattern is rooted. - $findPath = $joinedSegments - } elseif ($joinedSegments) { # The pattern is not rooted, and literal segements were found. - $findPath = [System.IO.Path]::Combine($DefaultRoot, $joinedSegments) - } else { # The pattern is not rooted, and no literal segements were found. - $findPath = $DefaultRoot - } - - # Clean up the path. - if ($findPath) { - $findPath = [System.IO.Path]::GetDirectoryName(([System.IO.Path]::Combine($findPath, '_'))) # Hack to remove unnecessary trailing slash. - $findPath = ConvertTo-NormalizedSeparators -Path $findPath - } - - return New-Object psobject -Property @{ - AdjustedPattern = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $Pattern - FindPath = $findPath - StatOnly = $literalSegments.Count -eq $parsedSegments.Count - } -} - -function Get-FindResult { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - $Options) - - if (!(Test-Path -LiteralPath $Path)) { - Write-Verbose 'Path not found.' - return - } - - $Path = ConvertTo-NormalizedSeparators -Path $Path - - # Push the first item. - [System.Collections.Stack]$stack = New-Object System.Collections.Stack - $stack.Push((Get-Item -LiteralPath $Path)) - - $count = 0 - while ($stack.Count) { - # Pop the next item and yield the result. - $item = $stack.Pop() - $count++ - $item.FullName - - # Traverse. - if (($item.Attributes -band 0x00000010) -eq 0x00000010) { # Directory - if (($item.Attributes -band 0x00000400) -ne 0x00000400 -or # ReparsePoint - $Options.FollowSymbolicLinks -or - ($count -eq 1 -and $Options.FollowSpecifiedSymbolicLink)) { - - $childItems = @( Get-DirectoryChildItem -Path $Item.FullName -Force ) - [System.Array]::Reverse($childItems) - foreach ($childItem in $childItems) { - $stack.Push($childItem) - } - } - } - } -} - -function Get-RootedPattern { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$DefaultRoot, - [Parameter(Mandatory = $true)] - [string]$Pattern) - - if ((Test-Rooted -Path $Pattern)) { - return $Pattern - } - - # Normalize root. - $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot - - # Escape special glob characters. - $DefaultRoot = $DefaultRoot -replace '(\[)(?=[^\/]+\])', '[[]' # Escape '[' when ']' follows within the path segment - $DefaultRoot = $DefaultRoot.Replace('?', '[?]') # Escape '?' - $DefaultRoot = $DefaultRoot.Replace('*', '[*]') # Escape '*' - $DefaultRoot = $DefaultRoot -replace '\+\(', '[+](' # Escape '+(' - $DefaultRoot = $DefaultRoot -replace '@\(', '[@](' # Escape '@(' - $DefaultRoot = $DefaultRoot -replace '!\(', '[!](' # Escape '!(' - - if ($DefaultRoot -like '[A-Z]:') { # e.g. C: - return "$DefaultRoot$Pattern" - } - - # Ensure root ends with a separator. - if (!$DefaultRoot.EndsWith('\')) { - $DefaultRoot = "$DefaultRoot\" - } - - return "$DefaultRoot$Pattern" -} - -function Test-Rooted { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path) - - $Path = ConvertTo-NormalizedSeparators -Path $Path - return $Path.StartsWith('\') -or # e.g. \ or \hello or \\hello - $Path -like '[A-Z]:*' # e.g. C: or C:\hello -} - -function Trace-MatchOptions { - [CmdletBinding()] - param($Options) - - Write-Verbose "MatchOptions.Dot: '$($Options.Dot)'" - Write-Verbose "MatchOptions.FlipNegate: '$($Options.FlipNegate)'" - Write-Verbose "MatchOptions.MatchBase: '$($Options.MatchBase)'" - Write-Verbose "MatchOptions.NoBrace: '$($Options.NoBrace)'" - Write-Verbose "MatchOptions.NoCase: '$($Options.NoCase)'" - Write-Verbose "MatchOptions.NoComment: '$($Options.NoComment)'" - Write-Verbose "MatchOptions.NoExt: '$($Options.NoExt)'" - Write-Verbose "MatchOptions.NoGlobStar: '$($Options.NoGlobStar)'" - Write-Verbose "MatchOptions.NoNegate: '$($Options.NoNegate)'" - Write-Verbose "MatchOptions.NoNull: '$($Options.NoNull)'" -} - -function Trace-FindOptions { - [CmdletBinding()] - param($Options) - - Write-Verbose "FindOptions.FollowSpecifiedSymbolicLink: '$($FindOptions.FollowSpecifiedSymbolicLink)'" - Write-Verbose "FindOptions.FollowSymbolicLinks: '$($FindOptions.FollowSymbolicLinks)'" -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/InputFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/InputFunctions.ps1 deleted file mode 100644 index 071d5ca..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/InputFunctions.ps1 +++ /dev/null @@ -1,524 +0,0 @@ -# Hash table of known variable info. The formatted env var name is the lookup key. -# -# The purpose of this hash table is to keep track of known variables. The hash table -# needs to be maintained for multiple reasons: -# 1) to distinguish between env vars and job vars -# 2) to distinguish between secret vars and public -# 3) to know the real variable name and not just the formatted env var name. -$script:knownVariables = @{ } -$script:vault = @{ } - -<# -.SYNOPSIS -Gets an endpoint. - -.DESCRIPTION -Gets an endpoint object for the specified endpoint name. The endpoint is returned as an object with three properties: Auth, Data, and Url. - -The Data property requires a 1.97 agent or higher. - -.PARAMETER Require -Writes an error to the error pipeline if the endpoint is not found. -#> -function Get-Endpoint { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [switch]$Require) - - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Get the URL. - $description = Get-LocString -Key PSLIB_EndpointUrl0 -ArgumentList $Name - $key = "ENDPOINT_URL_$Name" - $url = Get-VaultValue -Description $description -Key $key -Require:$Require - - # Get the auth object. - $description = Get-LocString -Key PSLIB_EndpointAuth0 -ArgumentList $Name - $key = "ENDPOINT_AUTH_$Name" - if ($auth = (Get-VaultValue -Description $description -Key $key -Require:$Require)) { - $auth = ConvertFrom-Json -InputObject $auth - } - - # Get the data. - $description = "'$Name' service endpoint data" - $key = "ENDPOINT_DATA_$Name" - if ($data = (Get-VaultValue -Description $description -Key $key)) { - $data = ConvertFrom-Json -InputObject $data - } - - # Return the endpoint. - if ($url -or $auth -or $data) { - New-Object -TypeName psobject -Property @{ - Url = $url - Auth = $auth - Data = $data - } - } - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } -} - -<# -.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. - -.DESCRIPTION -Gets the value for the specified input name. - -.PARAMETER AsBool -Returns the value as a bool. Returns true if the value converted to a string is "1" or "true" (case insensitive); otherwise false. - -.PARAMETER AsInt -Returns the value as an int. Returns the value converted to an int. Returns 0 if the conversion fails. - -.PARAMETER Default -Default value to use if the input is null or empty. - -.PARAMETER Require -Writes an error to the error pipeline if the input is null or empty. -#> -function Get-Input { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(ParameterSetName = 'Default')] - $Default, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [switch]$AsBool, - [switch]$AsInt) - - # Get the input from the vault. Splat the bound parameters hashtable. Splatting is required - # in order to concisely invoke the correct parameter set. - $null = $PSBoundParameters.Remove('Name') - $description = Get-LocString -Key PSLIB_Input0 -ArgumentList $Name - $key = "INPUT_$($Name.Replace(' ', '_').ToUpperInvariant())" - Get-VaultValue @PSBoundParameters -Description $description -Key $key -} - -<# -.SYNOPSIS -Gets a task variable. - -.DESCRIPTION -Gets the value for the specified task variable. - -.PARAMETER AsBool -Returns the value as a bool. Returns true if the value converted to a string is "1" or "true" (case insensitive); otherwise false. - -.PARAMETER AsInt -Returns the value as an int. Returns the value converted to an int. Returns 0 if the conversion fails. - -.PARAMETER Default -Default value to use if the input is null or empty. - -.PARAMETER Require -Writes an error to the error pipeline if the input is null or empty. -#> -function Get-TaskVariable { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(ParameterSetName = 'Default')] - $Default, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [switch]$AsBool, - [switch]$AsInt) - - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - $description = Get-LocString -Key PSLIB_TaskVariable0 -ArgumentList $Name - $variableKey = Get-VariableKey -Name $Name - if ($script:knownVariables.$variableKey.Secret) { - # Get secret variable. Splatting is required to concisely invoke the correct parameter set. - $null = $PSBoundParameters.Remove('Name') - $vaultKey = "SECRET_$variableKey" - Get-VaultValue @PSBoundParameters -Description $description -Key $vaultKey - } else { - # Get public variable. - $item = $null - $path = "Env:$variableKey" - if ((Test-Path -LiteralPath $path) -and ($item = Get-Item -LiteralPath $path).Value) { - # Intentionally empty. Value was successfully retrieved. - } elseif (!$script:nonInteractive) { - # The value wasn't found and the module is running in interactive dev mode. - # Prompt for the value. - Set-Item -LiteralPath $path -Value (Read-Host -Prompt $description) - if (Test-Path -LiteralPath $path) { - $item = Get-Item -LiteralPath $path - } - } - - # Get the converted value. Splatting is required to concisely invoke the correct parameter set. - $null = $PSBoundParameters.Remove('Name') - Get-Value @PSBoundParameters -Description $description -Key $variableKey -Value $item.Value - } - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } -} - -<# -.SYNOPSIS -Gets all job variables available to the task. Requires 2.104.1 agent or higher. - -.DESCRIPTION -Gets a snapshot of the current state of all job variables available to the task. -Requires a 2.104.1 agent or higher for full functionality. - -Returns an array of objects with the following properties: - [string]Name - [string]Value - [bool]Secret - -Limitations on an agent prior to 2.104.1: - 1) The return value does not include all public variables. Only public variables - that have been added using setVariable are returned. - 2) The name returned for each secret variable is the formatted environment variable - name, not the actual variable name (unless it was set explicitly at runtime using - setVariable). -#> -function Get-TaskVariableInfo { - [CmdletBinding()] - param() - - foreach ($info in $script:knownVariables.Values) { - New-Object -TypeName psobject -Property @{ - Name = $info.Name - Value = Get-TaskVariable -Name $info.Name - Secret = $info.Secret - } - } -} - -<# -.SYNOPSIS -Sets a task variable. - -.DESCRIPTION -Sets a task variable in the current task context as well as in the current job context. This allows the task variable to retrieved by subsequent tasks within the same job. -#> -function Set-TaskVariable { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [string]$Value, - [switch]$Secret) - - # Once a secret always a secret. - $variableKey = Get-VariableKey -Name $Name - [bool]$Secret = $Secret -or $script:knownVariables.$variableKey.Secret - if ($Secret) { - $vaultKey = "SECRET_$variableKey" - if (!$Value) { - # Clear the secret. - Write-Verbose "Set $Name = ''" - $script:vault.Remove($vaultKey) - } else { - # Store the secret in the vault. - Write-Verbose "Set $Name = '********'" - $script:vault[$vaultKey] = New-Object System.Management.Automation.PSCredential( - $vaultKey, - (ConvertTo-SecureString -String $Value -AsPlainText -Force)) - } - - # Clear the environment variable. - Set-Item -LiteralPath "Env:$variableKey" -Value '' - } else { - # Set the environment variable. - Write-Verbose "Set $Name = '$Value'" - Set-Item -LiteralPath "Env:$variableKey" -Value $Value - } - - # Store the metadata. - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - Name = $name - Secret = $Secret - } - - # Persist the variable in the task context. - Write-SetVariable -Name $Name -Value $Value -Secret:$Secret -} - -<# -.SYNOPSIS -Gets the value of an task feature and converts to a bool. - -.PARAMETER $FeatureName -Name of the feature to get. - -.NOTES -This method is only for internal Microsoft development. Do not use it for external tasks. -#> -function Get-PipelineFeature { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$FeatureName - ) - - $featureValue = Get-TaskVariable -Name "DistributedTask.Tasks.$FeatureName" - - if (!$featureValue) { - Write-Debug "Feature '$FeatureName' is not set. Defaulting to 'false'" - return $false - } - - $boolValue = $featureValue.ToLowerInvariant() -eq 'true' - - Write-Debug "Feature '$FeatureName' = '$featureValue'. Processed as '$boolValue'" - - return $boolValue -} - -######################################## -# Private functions. -######################################## -function Get-VaultValue { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$Description, - [Parameter(Mandatory = $true)] - [string]$Key, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [Parameter(ParameterSetName = 'Default')] - [object]$Default, - [switch]$AsBool, - [switch]$AsInt) - - # Attempt to get the vault value. - $value = $null - if ($psCredential = $script:vault[$Key]) { - $value = $psCredential.GetNetworkCredential().Password - } elseif (!$script:nonInteractive) { - # The value wasn't found. Prompt for the value if running in interactive dev mode. - $value = Read-Host -Prompt $Description - if ($value) { - $script:vault[$Key] = New-Object System.Management.Automation.PSCredential( - $Key, - (ConvertTo-SecureString -String $value -AsPlainText -Force)) - } - } - - Get-Value -Value $value @PSBoundParameters -} - -function Get-Value { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [string]$Value, - [Parameter(Mandatory = $true)] - [string]$Description, - [Parameter(Mandatory = $true)] - [string]$Key, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [Parameter(ParameterSetName = 'Default')] - [object]$Default, - [switch]$AsBool, - [switch]$AsInt) - - $result = $Value - if ($result) { - if ($Key -like 'ENDPOINT_AUTH_*') { - Write-Verbose "$($Key): '********'" - } else { - Write-Verbose "$($Key): '$result'" - } - } else { - Write-Verbose "$Key (empty)" - - # Write error if required. - if ($Require) { - Write-Error "$(Get-LocString -Key PSLIB_Required0 $Description)" - return - } - - # Fallback to the default if provided. - if ($PSCmdlet.ParameterSetName -eq 'Default') { - $result = $Default - $OFS = ' ' - Write-Verbose " Defaulted to: '$result'" - } else { - $result = '' - } - } - - # Convert to bool if specified. - if ($AsBool) { - if ($result -isnot [bool]) { - $result = "$result" -in '1', 'true' - Write-Verbose " Converted to bool: $result" - } - - return $result - } - - # Convert to int if specified. - if ($AsInt) { - if ($result -isnot [int]) { - try { - $result = [int]"$result" - } catch { - $result = 0 - } - - Write-Verbose " Converted to int: $result" - } - - return $result - } - - return $result -} - -function Initialize-Inputs { - # Store endpoints, inputs, and secret variables in the vault. - foreach ($variable in (Get-ChildItem -Path Env:ENDPOINT_?*, Env:INPUT_?*, Env:SECRET_?*, Env:SECUREFILE_?*)) { - # Record the secret variable metadata. This is required by Get-TaskVariable to - # retrieve the value. In a 2.104.1 agent or higher, this metadata will be overwritten - # when $env:VSTS_SECRET_VARIABLES is processed. - if ($variable.Name -like 'SECRET_?*') { - $variableKey = $variable.Name.Substring('SECRET_'.Length) - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - # This is technically not the variable name (has underscores instead of dots), - # but it's good enough to make Get-TaskVariable work in a pre-2.104.1 agent - # where $env:VSTS_SECRET_VARIABLES is not defined. - Name = $variableKey - Secret = $true - } - } - - # Store the value in the vault. - $vaultKey = $variable.Name - if ($variable.Value) { - $script:vault[$vaultKey] = New-Object System.Management.Automation.PSCredential( - $vaultKey, - (ConvertTo-SecureString -String $variable.Value -AsPlainText -Force)) - } - - # Clear the environment variable. - Remove-Item -LiteralPath "Env:$($variable.Name)" - } - - # Record the public variable names. Env var added in 2.104.1 agent. - if ($env:VSTS_PUBLIC_VARIABLES) { - foreach ($name in (ConvertFrom-Json -InputObject $env:VSTS_PUBLIC_VARIABLES)) { - $variableKey = Get-VariableKey -Name $name - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - Name = $name - Secret = $false - } - } - - $env:VSTS_PUBLIC_VARIABLES = '' - } - - # Record the secret variable names. Env var added in 2.104.1 agent. - if ($env:VSTS_SECRET_VARIABLES) { - foreach ($name in (ConvertFrom-Json -InputObject $env:VSTS_SECRET_VARIABLES)) { - $variableKey = Get-VariableKey -Name $name - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - Name = $name - Secret = $true - } - } - - $env:VSTS_SECRET_VARIABLES = '' - } -} - -function Get-VariableKey { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name) - - if ($Name -ne 'agent.jobstatus') { - $Name = $Name.Replace('.', '_') - } - - $Name.ToUpperInvariant() -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 deleted file mode 100644 index 9e9e9ec..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 +++ /dev/null @@ -1,320 +0,0 @@ -<# -.SYNOPSIS -Finds files or directories. - -.DESCRIPTION -Finds files or directories using advanced pattern matching. - -.PARAMETER LiteralDirectory -Directory to search. - -.PARAMETER LegacyPattern -Proprietary pattern format. The LiteralDirectory parameter is used to root any unrooted patterns. - -Separate multiple patterns using ";". Escape actual ";" in the path by using ";;". -"?" indicates a wildcard that represents any single character within a path segment. -"*" indicates a wildcard that represents zero or more characters within a path segment. -"**" as the entire path segment indicates a recursive search. -"**" within a path segment indicates a recursive intersegment wildcard. -"+:" (can be omitted) indicates an include pattern. -"-:" indicates an exclude pattern. - -The result is from the command is a union of all the matches from the include patterns, minus the matches from the exclude patterns. - -.PARAMETER IncludeFiles -Indicates whether to include files in the results. - -If neither IncludeFiles or IncludeDirectories is set, then IncludeFiles is assumed. - -.PARAMETER IncludeDirectories -Indicates whether to include directories in the results. - -If neither IncludeFiles or IncludeDirectories is set, then IncludeFiles is assumed. - -.PARAMETER Force -Indicates whether to include hidden items. - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\Is?Match.txt" - -Given: -C:\Directory\Is1Match.txt -C:\Directory\Is2Match.txt -C:\Directory\IsNotMatch.txt - -Returns: -C:\Directory\Is1Match.txt -C:\Directory\Is2Match.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\Is*Match.txt" - -Given: -C:\Directory\IsOneMatch.txt -C:\Directory\IsTwoMatch.txt -C:\Directory\NonMatch.txt - -Returns: -C:\Directory\IsOneMatch.txt -C:\Directory\IsTwoMatch.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\**\Match.txt" - -Given: -C:\Directory\Match.txt -C:\Directory\NotAMatch.txt -C:\Directory\SubDir\Match.txt -C:\Directory\SubDir\SubSubDir\Match.txt - -Returns: -C:\Directory\Match.txt -C:\Directory\SubDir\Match.txt -C:\Directory\SubDir\SubSubDir\Match.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\**" - -Given: -C:\Directory\One.txt -C:\Directory\SubDir\Two.txt -C:\Directory\SubDir\SubSubDir\Three.txt - -Returns: -C:\Directory\One.txt -C:\Directory\SubDir\Two.txt -C:\Directory\SubDir\SubSubDir\Three.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\Sub**Match.txt" - -Given: -C:\Directory\IsNotAMatch.txt -C:\Directory\SubDir\IsAMatch.txt -C:\Directory\SubDir\IsNot.txt -C:\Directory\SubDir\SubSubDir\IsAMatch.txt -C:\Directory\SubDir\SubSubDir\IsNot.txt - -Returns: -C:\Directory\SubDir\IsAMatch.txt -C:\Directory\SubDir\SubSubDir\IsAMatch.txt -#> -function Find-Files { - [CmdletBinding()] - param( - [ValidateNotNullOrEmpty()] - [Parameter()] - [string]$LiteralDirectory, - [Parameter(Mandatory = $true)] - [string]$LegacyPattern, - [switch]$IncludeFiles, - [switch]$IncludeDirectories, - [switch]$Force) - - # Note, due to subtle implementation details of Get-PathPrefix/Get-PathIterator, - # this function does not appear to be able to search the root of a drive and other - # cases where Path.GetDirectoryName() returns empty. More details in Get-PathPrefix. - - Trace-EnteringInvocation $MyInvocation - if (!$IncludeFiles -and !$IncludeDirectories) { - $IncludeFiles = $true - } - - $includePatterns = New-Object System.Collections.Generic.List[string] - $excludePatterns = New-Object System.Collections.Generic.List[System.Text.RegularExpressions.Regex] - $LegacyPattern = $LegacyPattern.Replace(';;', "`0") - foreach ($pattern in $LegacyPattern.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)) { - $pattern = $pattern.Replace("`0", ';') - $isIncludePattern = Test-IsIncludePattern -Pattern ([ref]$pattern) - if ($LiteralDirectory -and !([System.IO.Path]::IsPathRooted($pattern))) { - # Use the root directory provided to make the pattern a rooted path. - $pattern = [System.IO.Path]::Combine($LiteralDirectory, $pattern) - } - - # Validate pattern does not end with a \. - if ($pattern.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { - throw (Get-LocString -Key PSLIB_InvalidPattern0 -ArgumentList $pattern) - } - - if ($isIncludePattern) { - $includePatterns.Add($pattern) - } else { - $excludePatterns.Add((Convert-PatternToRegex -Pattern $pattern)) - } - } - - $count = 0 - foreach ($path in (Get-MatchingItems -IncludePatterns $includePatterns -ExcludePatterns $excludePatterns -IncludeFiles:$IncludeFiles -IncludeDirectories:$IncludeDirectories -Force:$Force)) { - $count++ - $path - } - - Write-Verbose "Total found: $count" - Trace-LeavingInvocation $MyInvocation -} - -######################################## -# Private functions. -######################################## -function Convert-PatternToRegex { - [CmdletBinding()] - param([string]$Pattern) - - $Pattern = [regex]::Escape($Pattern.Replace('\', '/')). # Normalize separators and regex escape. - Replace('/\*\*/', '((/.+/)|(/))'). # Replace directory globstar. - Replace('\*\*', '.*'). # Replace remaining globstars with a wildcard that can span directory separators. - Replace('\*', '[^/]*'). # Replace asterisks with a wildcard that cannot span directory separators. - # bug: should be '[^/]' instead of '.' - Replace('\?', '.') # Replace single character wildcards. - New-Object regex -ArgumentList "^$Pattern`$", ([System.Text.RegularExpressions.RegexOptions]::IgnoreCase) -} - -function Get-FileNameFilter { - [CmdletBinding()] - param([string]$Pattern) - - $index = $Pattern.LastIndexOf('\') - if ($index -eq -1 -or # Pattern does not contain a backslash. - !($Pattern = $Pattern.Substring($index + 1)) -or # Pattern ends in a backslash. - $Pattern.Contains('**')) # Last segment contains an inter-segment wildcard. - { - return '*' - } - - # bug? is this supposed to do substring? - return $Pattern -} - -function Get-MatchingItems { - [CmdletBinding()] - param( - [System.Collections.Generic.List[string]]$IncludePatterns, - [System.Collections.Generic.List[regex]]$ExcludePatterns, - [switch]$IncludeFiles, - [switch]$IncludeDirectories, - [switch]$Force) - - Trace-EnteringInvocation $MyInvocation - $allFiles = New-Object System.Collections.Generic.HashSet[string] - foreach ($pattern in $IncludePatterns) { - $pathPrefix = Get-PathPrefix -Pattern $pattern - $fileNameFilter = Get-FileNameFilter -Pattern $pattern - $patternRegex = Convert-PatternToRegex -Pattern $pattern - # Iterate over the directories and files under the pathPrefix. - Get-PathIterator -Path $pathPrefix -Filter $fileNameFilter -IncludeFiles:$IncludeFiles -IncludeDirectories:$IncludeDirectories -Force:$Force | - ForEach-Object { - # Normalize separators. - $normalizedPath = $_.Replace('\', '/') - # **/times/** will not match C:/fun/times because there isn't a trailing slash. - # So try both if including directories. - $alternatePath = "$normalizedPath/" # potential bug: it looks like this will result in a false - # positive if the item is a regular file and not a directory - - $isMatch = $false - if ($patternRegex.IsMatch($normalizedPath) -or ($IncludeDirectories -and $patternRegex.IsMatch($alternatePath))) { - $isMatch = $true - - # Test whether the path should be excluded. - foreach ($regex in $ExcludePatterns) { - if ($regex.IsMatch($normalizedPath) -or ($IncludeDirectories -and $regex.IsMatch($alternatePath))) { - $isMatch = $false - break - } - } - } - - if ($isMatch) { - $null = $allFiles.Add($_) - } - } - } - - Trace-Path -Path $allFiles -PassThru - Trace-LeavingInvocation $MyInvocation -} - -function Get-PathIterator { - [CmdletBinding()] - param( - [string]$Path, - [string]$Filter, - [switch]$IncludeFiles, - [switch]$IncludeDirectories, - [switch]$Force) - - if (!$Path) { - return - } - - # bug: this returns the dir without verifying whether exists - if ($IncludeDirectories) { - $Path - } - - Get-DirectoryChildItem -Path $Path -Filter $Filter -Force:$Force -Recurse | - ForEach-Object { - if ($_.Attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { - if ($IncludeDirectories) { - $_.FullName - } - } elseif ($IncludeFiles) { - $_.FullName - } - } -} - -function Get-PathPrefix { - [CmdletBinding()] - param([string]$Pattern) - - # Note, unable to search root directories is a limitation due to subtleties of this function - # and downstream code in Get-PathIterator that short-circuits when the path prefix is empty. - # This function uses Path.GetDirectoryName() to determine the path prefix, which will yield - # empty in some cases. See the following examples of Path.GetDirectoryName() input => output: - # C:/ => - # C:/hello => C:\ - # C:/hello/ => C:\hello - # C:/hello/world => C:\hello - # C:/hello/world/ => C:\hello\world - # C: => - # C:hello => C: - # C:hello/ => C:hello - # / => - # /hello => \ - # /hello/ => \hello - # //hello => - # //hello/ => - # //hello/world => - # //hello/world/ => \\hello\world - - $index = $Pattern.IndexOfAny([char[]]@('*'[0], '?'[0])) - if ($index -eq -1) { - # If no wildcards are found, return the directory name portion of the path. - # If there is no directory name (file name only in pattern), this will return empty string. - return [System.IO.Path]::GetDirectoryName($Pattern) - } - - [System.IO.Path]::GetDirectoryName($Pattern.Substring(0, $index)) -} - -function Test-IsIncludePattern { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [ref]$Pattern) - - # Include patterns start with +: or anything except -: - # Exclude patterns start with -: - if ($Pattern.value.StartsWith("+:")) { - # Remove the prefix. - $Pattern.value = $Pattern.value.Substring(2) - $true - } elseif ($Pattern.value.StartsWith("-:")) { - # Remove the prefix. - $Pattern.value = $Pattern.value.Substring(2) - $false - } else { - # No prefix, so leave the string alone. - $true; - } -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 deleted file mode 100644 index b554970..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 +++ /dev/null @@ -1,150 +0,0 @@ -$script:resourceStrings = @{ } - -<# -.SYNOPSIS -Gets a localized resource string. - -.DESCRIPTION -Gets a localized resource string and optionally formats the string with arguments. - -If the format fails (due to a bad format string or incorrect expected arguments in the format string), then the format string is returned followed by each of the arguments (delimited by a space). - -If the lookup key is not found, then the lookup key is returned followed by each of the arguments (delimited by a space). - -.PARAMETER Require -Writes an error to the error pipeline if the endpoint is not found. -#> -function Get-LocString { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true, Position = 1)] - [string]$Key, - [Parameter(Position = 2)] - [object[]]$ArgumentList = @( )) - - # Due to the dynamically typed nature of PowerShell, a single null argument passed - # to an array parameter is interpreted as a null array. - if ([object]::ReferenceEquals($null, $ArgumentList)) { - $ArgumentList = @( $null ) - } - - # Lookup the format string. - $format = '' - if (!($format = $script:resourceStrings[$Key])) { - # Warn the key was not found. Prevent recursion if the lookup key is the - # "string resource key not found" lookup key. - $resourceNotFoundKey = 'PSLIB_StringResourceKeyNotFound0' - if ($key -ne $resourceNotFoundKey) { - Write-Warning (Get-LocString -Key $resourceNotFoundKey -ArgumentList $Key) - } - - # Fallback to just the key itself if there aren't any arguments to format. - if (!$ArgumentList.Count) { return $key } - - # Otherwise fallback to the key followed by the arguments. - $OFS = " " - return "$key $ArgumentList" - } - - # Return the string if there aren't any arguments to format. - if (!$ArgumentList.Count) { return $format } - - try { - [string]::Format($format, $ArgumentList) - } catch { - Write-Warning (Get-LocString -Key 'PSLIB_StringFormatFailed') - $OFS = " " - "$format $ArgumentList" - } -} - -<# -.SYNOPSIS -Imports resource strings for use with GetVstsLocString. - -.DESCRIPTION -Imports resource strings for use with GetVstsLocString. The imported strings are stored in an internal resource string dictionary. Optionally, if a separate resource file for the current culture exists, then the localized strings from that file then imported (overlaid) into the same internal resource string dictionary. - -Resource strings from the SDK are prefixed with "PSLIB_". This prefix should be avoided for custom resource strings. - -.Parameter LiteralPath -JSON file containing resource strings. - -.EXAMPLE -Import-VstsLocStrings -LiteralPath $PSScriptRoot\Task.json - -Imports strings from messages section in the JSON file. If a messages section is not defined, then no strings are imported. Example messages section: -{ - "messages": { - "Hello": "Hello you!", - "Hello0": "Hello {0}!" - } -} - -.EXAMPLE -Import-VstsLocStrings -LiteralPath $PSScriptRoot\Task.json - -Overlays strings from an optional separate resource file for the current culture. - -Given the task variable System.Culture is set to 'de-DE'. This variable is set by the agent based on the current culture for the job. -Given the file Task.json contains: -{ - "messages": { - "GoodDay": "Good day!", - } -} -Given the file resources.resjson\de-DE\resources.resjson: -{ - "loc.messages.GoodDay": "Guten Tag!" -} - -The net result from the import command would be one new key-value pair added to the internal dictionary: Key = 'GoodDay', Value = 'Guten Tag!' -#> -function Import-LocStrings { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$LiteralPath) - - # Validate the file exists. - if (!(Test-Path -LiteralPath $LiteralPath -PathType Leaf)) { - Write-Warning (Get-LocString -Key PSLIB_FileNotFound0 -ArgumentList $LiteralPath) - return - } - - # Load the json. - Write-Verbose "Loading resource strings from: $LiteralPath" - $count = 0 - if ($messages = (Get-Content -LiteralPath $LiteralPath -Encoding UTF8 | Out-String | ConvertFrom-Json).messages) { - # Add each resource string to the hashtable. - foreach ($member in (Get-Member -InputObject $messages -MemberType NoteProperty)) { - [string]$key = $member.Name - $script:resourceStrings[$key] = $messages."$key" - $count++ - } - } - - Write-Verbose "Loaded $count strings." - - # Get the culture. - $culture = Get-TaskVariable -Name "System.Culture" -Default "en-US" - - # Load the resjson. - $resjsonPath = "$([System.IO.Path]::GetDirectoryName($LiteralPath))\Strings\resources.resjson\$culture\resources.resjson" - if (Test-Path -LiteralPath $resjsonPath) { - Write-Verbose "Loading resource strings from: $resjsonPath" - $count = 0 - $resjson = Get-Content -LiteralPath $resjsonPath -Encoding UTF8 | Out-String | ConvertFrom-Json - foreach ($member in (Get-Member -Name loc.messages.* -InputObject $resjson -MemberType NoteProperty)) { - if (!($value = $resjson."$($member.Name)")) { - continue - } - - [string]$key = $member.Name.Substring('loc.messages.'.Length) - $script:resourceStrings[$key] = $value - $count++ - } - - Write-Verbose "Loaded $count strings." - } -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 deleted file mode 100644 index 90400c7..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 +++ /dev/null @@ -1,634 +0,0 @@ -$script:loggingCommandPrefix = '##vso[' -$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: Add test to verify don't need to escape "=". - -$commandCorrelationId = $env:COMMAND_CORRELATION_ID -if ($null -ne $commandCorrelationId) -{ - [System.Environment]::SetEnvironmentVariable("COMMAND_CORRELATION_ID", $null) -} - -$IssueSources = @{ - CustomerScript = "CustomerScript" - TaskInternal = "TaskInternal" -} - -<# -.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-AddAttachment { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Type, - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$Path, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'addattachment' -Data $Path -Properties @{ - 'type' = $Type - 'name' = $Name - } -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-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. -#> -function Write-AddBuildTag { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Value, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'build' -Event 'addbuildtag' -Data $Value -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-AssociateArtifact { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$Type, - [hashtable]$Properties, - [switch]$AsOutput) - - $p = @{ } - if ($Properties) { - foreach ($key in $Properties.Keys) { - $p[$key] = $Properties[$key] - } - } - - $p['artifactname'] = $Name - $p['artifacttype'] = $Type - Write-LoggingCommand -Area 'artifact' -Event 'associate' -Data $Path -Properties $p -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-LogDetail { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [guid]$Id, - $ParentId, - [string]$Type, - [string]$Name, - $Order, - $StartTime, - $FinishTime, - $Progress, - [ValidateSet('Unknown', 'Initialized', 'InProgress', 'Completed')] - [Parameter()] - $State, - [ValidateSet('Succeeded', 'SucceededWithIssues', 'Failed', 'Cancelled', 'Skipped')] - [Parameter()] - $Result, - [string]$Message, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'logdetail' -Data $Message -Properties @{ - 'id' = $Id - 'parentid' = $ParentId - 'type' = $Type - 'name' = $Name - 'order' = $Order - 'starttime' = $StartTime - 'finishtime' = $FinishTime - 'progress' = $Progress - 'state' = $State - 'result' = $Result - } -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-SetProgress { - [CmdletBinding()] - param( - [ValidateRange(0, 100)] - [Parameter(Mandatory = $true)] - [int]$Percent, - [string]$CurrentOperation, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'setprogress' -Data $CurrentOperation -Properties @{ - 'value' = $Percent - } -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-SetResult { - [CmdletBinding(DefaultParameterSetName = 'AsOutput')] - param( - [ValidateSet("Succeeded", "SucceededWithIssues", "Failed", "Cancelled", "Skipped")] - [Parameter(Mandatory = $true)] - [string]$Result, - [string]$Message, - [Parameter(ParameterSetName = 'AsOutput')] - [switch]$AsOutput, - [Parameter(ParameterSetName = 'DoNotThrow')] - [switch]$DoNotThrow) - - Write-LoggingCommand -Area 'task' -Event 'complete' -Data $Message -Properties @{ - 'result' = $Result - } -AsOutput:$AsOutput - if ($Result -eq 'Failed' -and !$AsOutput -and !$DoNotThrow) { - # Special internal exception type to control the flow. Not currently intended - # for public usage and subject to change. - throw (New-Object VstsTaskSdk.TerminationException($Message)) - } -} - -<# -.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-SetSecret { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Value, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'setsecret' -Data $Value -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-SetVariable { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [string]$Value, - [switch]$Secret, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $Value -Properties @{ - 'variable' = $Name - 'issecret' = $Secret - } -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-TaskDebug { - [CmdletBinding()] - param( - [string]$Message, - [switch]$AsOutput) - - Write-TaskDebug_Internal @PSBoundParameters -} - -<# -.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-TaskError { - [CmdletBinding()] - param( - [string]$Message, - [string]$ErrCode, - [string]$SourcePath, - [string]$LineNumber, - [string]$ColumnNumber, - [switch]$AsOutput, - [string]$IssueSource, - [string]$AuditAction - ) - - Write-LogIssue -Type error @PSBoundParameters -} - -<# -.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-TaskVerbose { - [CmdletBinding()] - param( - [string]$Message, - [switch]$AsOutput) - - Write-TaskDebug_Internal @PSBoundParameters -AsVerbose -} - -<# -.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-TaskWarning { - [CmdletBinding()] - param( - [string]$Message, - [string]$ErrCode, - [string]$SourcePath, - [string]$LineNumber, - [string]$ColumnNumber, - [switch]$AsOutput, - [string]$IssueSource, - [string]$AuditAction - ) - - Write-LogIssue -Type warning @PSBoundParameters -} - -<# -.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. -#> -function Write-UpdateBuildNumber { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Value, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'build' -Event 'updatebuildnumber' -Data $Value -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-UploadArtifact { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$ContainerFolder, - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$Path, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'artifact' -Event 'upload' -Data $Path -Properties @{ - 'containerfolder' = $ContainerFolder - 'artifactname' = $Name - } -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-UploadBuildLog { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [switch]$AsOutput) - - 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 -} - -<# -.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-LoggingCommand { - [CmdletBinding(DefaultParameterSetName = 'Parameters')] - param( - [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] - [string]$Area, - [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] - [string]$Event, - [Parameter(ParameterSetName = 'Parameters')] - [string]$Data, - [Parameter(ParameterSetName = 'Parameters')] - [hashtable]$Properties, - [Parameter(Mandatory = $true, ParameterSetName = 'Object')] - $Command, - [switch]$AsOutput) - - if ($PSCmdlet.ParameterSetName -eq 'Object') { - Write-LoggingCommand -Area $Command.Area -Event $Command.Event -Data $Command.Data -Properties $Command.Properties -AsOutput:$AsOutput - return - } - - $command = Format-LoggingCommand -Area $Area -Event $Event -Data $Data -Properties $Properties - if ($AsOutput) { - $command - } else { - Write-Host $command - } -} - -######################################## -# Private functions. -######################################## -function Format-LoggingCommandData { - [CmdletBinding()] - param([string]$Value, [switch]$Reverse) - - if (!$Value) { - return '' - } - - if (!$Reverse) { - foreach ($mapping in $script:loggingCommandEscapeMappings) { - $Value = $Value.Replace($mapping.Token, $mapping.Replacement) - } - } else { - for ($i = $script:loggingCommandEscapeMappings.Length - 1 ; $i -ge 0 ; $i--) { - $mapping = $script:loggingCommandEscapeMappings[$i] - $Value = $Value.Replace($mapping.Replacement, $mapping.Token) - } - } - - return $Value -} - -function Format-LoggingCommand { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Area, - [Parameter(Mandatory = $true)] - [string]$Event, - [string]$Data, - [System.Collections.IDictionary]$Properties) - - # Append the preamble. - [System.Text.StringBuilder]$sb = New-Object -TypeName System.Text.StringBuilder - $null = $sb.Append($script:loggingCommandPrefix).Append($Area).Append('.').Append($Event) - - # Append the properties. - if ($Properties) { - $first = $true - foreach ($key in $Properties.Keys) { - [string]$value = Format-LoggingCommandData $Properties[$key] - if ($value) { - if ($first) { - $null = $sb.Append(' ') - $first = $false - } else { - $null = $sb.Append(';') - } - - $null = $sb.Append("$key=$value") - } - } - } - - # Append the tail and output the value. - $Data = Format-LoggingCommandData $Data - $sb.Append(']').Append($Data).ToString() -} - -function Write-LogIssue { - [CmdletBinding()] - param( - [ValidateSet('warning', 'error')] - [Parameter(Mandatory = $true)] - [string]$Type, - [string]$Message, - [string]$ErrCode, - [string]$SourcePath, - [string]$LineNumber, - [string]$ColumnNumber, - [switch]$AsOutput, - [AllowNull()] - [ValidateSet('CustomerScript', 'TaskInternal')] - [string]$IssueSource = $IssueSources.TaskInternal, - [string]$AuditAction - ) - - $properties = [ordered]@{ - 'type' = $Type - 'code' = $ErrCode - 'sourcepath' = $SourcePath - 'linenumber' = $LineNumber - 'columnnumber' = $ColumnNumber - 'source' = $IssueSource - 'correlationId' = $commandCorrelationId - 'auditAction' = $AuditAction - } - $command = Format-LoggingCommand -Area 'task' -Event 'logissue' -Data $Message -Properties $properties - if ($AsOutput) { - return $command - } - - if ($Type -eq 'error') { - $foregroundColor = $host.PrivateData.ErrorForegroundColor - $backgroundColor = $host.PrivateData.ErrorBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::Red - $backgroundColor = [System.ConsoleColor]::Black - } - } else { - $foregroundColor = $host.PrivateData.WarningForegroundColor - $backgroundColor = $host.PrivateData.WarningBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::Yellow - $backgroundColor = [System.ConsoleColor]::Black - } - } - - Write-Host $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor -} - -function Write-TaskDebug_Internal { - [CmdletBinding()] - param( - [string]$Message, - [switch]$AsVerbose, - [switch]$AsOutput) - - $command = Format-LoggingCommand -Area 'task' -Event 'debug' -Data $Message - if ($AsOutput) { - return $command - } - - if ($AsVerbose) { - $foregroundColor = $host.PrivateData.VerboseForegroundColor - $backgroundColor = $host.PrivateData.VerboseBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::Cyan - $backgroundColor = [System.ConsoleColor]::Black - } - } else { - $foregroundColor = $host.PrivateData.DebugForegroundColor - $backgroundColor = $host.PrivateData.DebugBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::DarkGray - $backgroundColor = [System.ConsoleColor]::Black - } - } - - Write-Host -Object $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 deleted file mode 100644 index 51cda34..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 +++ /dev/null @@ -1,205 +0,0 @@ -######################################## -# Private functions. -######################################## -function ConvertFrom-LongFormPath { - [CmdletBinding()] - param([string]$Path) - - if ($Path) { - if ($Path.StartsWith('\\?\UNC')) { - # E.g. \\?\UNC\server\share -> \\server\share - return $Path.Substring(1, '\?\UNC'.Length) - } elseif ($Path.StartsWith('\\?\')) { - # E.g. \\?\C:\directory -> C:\directory - return $Path.Substring('\\?\'.Length) - } - } - - return $Path -} -function ConvertTo-LongFormPath { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path) - - [string]$longFormPath = Get-FullNormalizedPath -Path $Path - if ($longFormPath -and !$longFormPath.StartsWith('\\?')) { - if ($longFormPath.StartsWith('\\')) { - # E.g. \\server\share -> \\?\UNC\server\share - return "\\?\UNC$($longFormPath.Substring(1))" - } else { - # E.g. C:\directory -> \\?\C:\directory - return "\\?\$longFormPath" - } - } - - return $longFormPath -} - -# TODO: ADD A SWITCH TO EXCLUDE FILES, A SWITCH TO EXCLUDE DIRECTORIES, AND A SWITCH NOT TO FOLLOW REPARSE POINTS. -function Get-DirectoryChildItem { - [CmdletBinding()] - param( - [string]$Path, - [ValidateNotNullOrEmpty()] - [Parameter()] - [string]$Filter = "*", - [switch]$Force, - [VstsTaskSdk.FS.FindFlags]$Flags = [VstsTaskSdk.FS.FindFlags]::LargeFetch, - [VstsTaskSdk.FS.FindInfoLevel]$InfoLevel = [VstsTaskSdk.FS.FindInfoLevel]::Basic, - [switch]$Recurse) - - $stackOfDirectoryQueues = New-Object System.Collections.Stack - while ($true) { - $directoryQueue = New-Object System.Collections.Queue - $fileQueue = New-Object System.Collections.Queue - $findData = New-Object VstsTaskSdk.FS.FindData - $longFormPath = (ConvertTo-LongFormPath $Path) - $handle = $null - try { - $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( - [System.IO.Path]::Combine($longFormPath, $Filter), - $InfoLevel, - $findData, - [VstsTaskSdk.FS.FindSearchOps]::NameMatch, - [System.IntPtr]::Zero, - $Flags) - if (!$handle.IsInvalid) { - while ($true) { - if ($findData.fileName -notin '.', '..') { - $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes - # If the item is hidden, check if $Force is specified. - if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { - # Create the item. - $item = New-Object -TypeName psobject -Property @{ - 'Attributes' = $attributes - 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) - 'Name' = $findData.fileName - } - # Output directories immediately. - if ($item.Attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { - $item - # Append to the directory queue if recursive and default filter. - if ($Recurse -and $Filter -eq '*') { - $directoryQueue.Enqueue($item) - } - } else { - # Hold the files until all directories have been output. - $fileQueue.Enqueue($item) - } - } - } - - if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } - - if ($handle.IsInvalid) { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path - )) - } - } - } - } finally { - if ($handle -ne $null) { $handle.Dispose() } - } - - # If recursive and non-default filter, queue child directories. - if ($Recurse -and $Filter -ne '*') { - $findData = New-Object VstsTaskSdk.FS.FindData - $handle = $null - try { - $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( - [System.IO.Path]::Combine($longFormPath, '*'), - [VstsTaskSdk.FS.FindInfoLevel]::Basic, - $findData, - [VstsTaskSdk.FS.FindSearchOps]::NameMatch, - [System.IntPtr]::Zero, - $Flags) - if (!$handle.IsInvalid) { - while ($true) { - if ($findData.fileName -notin '.', '..') { - $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes - # If the item is hidden, check if $Force is specified. - if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { - # Collect directories only. - if ($attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { - # Create the item. - $item = New-Object -TypeName psobject -Property @{ - 'Attributes' = $attributes - 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) - 'Name' = $findData.fileName - } - $directoryQueue.Enqueue($item) - } - } - } - - if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } - - if ($handle.IsInvalid) { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path - )) - } - } - } - } finally { - if ($handle -ne $null) { $handle.Dispose() } - } - } - - # Output the files. - $fileQueue - - # Push the directory queue onto the stack if any directories were found. - if ($directoryQueue.Count) { $stackOfDirectoryQueues.Push($directoryQueue) } - - # Break out of the loop if no more directory queues to process. - if (!$stackOfDirectoryQueues.Count) { break } - - # Get the next path. - $directoryQueue = $stackOfDirectoryQueues.Peek() - $Path = $directoryQueue.Dequeue().FullName - - # Pop the directory queue if it's empty. - if (!$directoryQueue.Count) { $null = $stackOfDirectoryQueues.Pop() } - } -} - -function Get-FullNormalizedPath { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path) - - [string]$outPath = $Path - [uint32]$bufferSize = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, 0, $null, $null) - [int]$lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - if ($bufferSize -gt 0) { - $absolutePath = New-Object System.Text.StringBuilder([int]$bufferSize) - [uint32]$length = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, $bufferSize, $absolutePath, $null) - $lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - if ($length -gt 0) { - $outPath = $absolutePath.ToString() - } else { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - $lastWin32Error - Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path - )) - } - } else { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - $lastWin32Error - Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path - )) - } - - if ($outPath.EndsWith('\') -and !$outPath.EndsWith(':\')) { - $outPath = $outPath.TrimEnd('\') - } - - $outPath -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Minimatch.dll b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Minimatch.dll deleted file mode 100644 index 700ddc426e56b08270c9dcbb1ade8ded248f98d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18432 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 X?dtd6e<)43|K{fYzxw~TJn(-2*CUr_ diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/OutFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/OutFunctions.ps1 deleted file mode 100644 index ce9160b..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/OutFunctions.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -# TODO: It would be better if the Out-Default function resolved the underlying Out-Default -# command in the begin block. This would allow for supporting other modules that override -# Out-Default. -$script:outDefaultCmdlet = $ExecutionContext.InvokeCommand.GetCmdlet("Microsoft.PowerShell.Core\Out-Default") - -######################################## -# Public functions. -######################################## -function Out-Default { - [CmdletBinding(ConfirmImpact = "Medium")] - param( - [Parameter(ValueFromPipeline = $true)] - [System.Management.Automation.PSObject]$InputObject) - - begin { - #Write-Host '[Entering Begin Out-Default]' - $__sp = { & $script:outDefaultCmdlet @PSBoundParameters }.GetSteppablePipeline() - $__sp.Begin($pscmdlet) - #Write-Host '[Leaving Begin Out-Default]' - } - - process { - #Write-Host '[Entering Process Out-Default]' - if ($_ -is [System.Management.Automation.ErrorRecord]) { - Write-Verbose -Message 'Error record:' 4>&1 | Out-Default - Write-Verbose -Message (Remove-TrailingNewLine (Out-String -InputObject $_ -Width 2147483647)) 4>&1 | Out-Default - Write-Verbose -Message 'Script stack trace:' 4>&1 | Out-Default - Write-Verbose -Message "$($_.ScriptStackTrace)" 4>&1 | Out-Default - Write-Verbose -Message 'Exception:' 4>&1 | Out-Default - Write-Verbose -Message $_.Exception.ToString() 4>&1 | Out-Default - Write-TaskError -Message $_.Exception.Message -IssueSource $IssueSources.TaskInternal - } elseif ($_ -is [System.Management.Automation.WarningRecord]) { - Write-TaskWarning -Message (Remove-TrailingNewLine (Out-String -InputObject $_ -Width 2147483647)) -IssueSource $IssueSources.TaskInternal - } elseif ($_ -is [System.Management.Automation.VerboseRecord] -and !$global:__vstsNoOverrideVerbose) { - foreach ($private:str in (Format-DebugMessage -Object $_)) { - Write-TaskVerbose -Message $private:str - } - } elseif ($_ -is [System.Management.Automation.DebugRecord] -and !$global:__vstsNoOverrideVerbose) { - foreach ($private:str in (Format-DebugMessage -Object $_)) { - Write-TaskDebug -Message $private:str - } - } else { -# TODO: Consider using out-string here to control the width. As a security precaution it would actually be best to set it to max so wrapping doesn't interfere with secret masking. - $__sp.Process($_) - } - - #Write-Host '[Leaving Process Out-Default]' - } - - end { - #Write-Host '[Entering End Out-Default]' - $__sp.End() - #Write-Host '[Leaving End Out-Default]' - } -} - -######################################## -# Private functions. -######################################## -function Format-DebugMessage { - [CmdletBinding()] - param([psobject]$Object) - - $private:str = Out-String -InputObject $Object -Width 2147483647 - $private:str = Remove-TrailingNewLine $private:str - "$private:str".Replace("`r`n", "`n").Replace("`r", "`n").Split("`n"[0]) -} - -function Remove-TrailingNewLine { - [CmdletBinding()] - param($Str) - if ([object]::ReferenceEquals($Str, $null)) { - return $Str - } elseif ($Str.EndsWith("`r`n")) { - return $Str.Substring(0, $Str.Length - 2) - } else { - return $Str - } -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml deleted file mode 100644 index d930995..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - Microsoft.PowerShell.Commands.PSRepositoryItemInfo - System.Management.Automation.PSCustomObject - System.Object - - - VstsTaskSdk - 0.21.0 - Module - VSTS Task SDK - Microsoft Corporation - - - System.Object[] - System.Array - System.Object - - - VSTS - martinmrazik - EDergachev - tramsing - - - (c) Microsoft Corporation. All rights reserved. -

2024-05-02T10:19:39-06:00
- - - - https://github.com/Microsoft/azure-pipelines-task-lib - - - - - PSModule - - - - - System.Collections.Hashtable - System.Object - - - - Command - - - - - - - DscResource - - - - RoleCapability - - - - Function - - - - Workflow - - - - Cmdlet - - - - - - - - - - - https://www.powershellgallery.com/api/v2 - PSGallery - NuGet - - - System.Management.Automation.PSCustomObject - System.Object - - - (c) Microsoft Corporation. All rights reserved. - VSTS Task SDK - False - True - True - 88436 - 684804 - 67529 - 5/2/2024 10:19:39 -06:00 - 5/2/2024 10:19:39 -06:00 - 5/22/2025 21:30:00 -06:00 - PSModule - False - 2025-05-22T21:30:00Z - 0.21.0 - Microsoft Corporation - false - Module - VstsTaskSdk.nuspec|FindFunctions.ps1|LegacyFindFunctions.ps1|LocalizationFunctions.ps1|LongPathFunctions.ps1|OutFunctions.ps1|ToolFunctions.ps1|VstsTaskSdk.dll|VstsTaskSdk.psd1|Strings\resources.resjson\de-DE\resources.resjson|Strings\resources.resjson\es-ES\resources.resjson|Strings\resources.resjson\it-IT\resources.resjson|Strings\resources.resjson\ja-JP\resources.resjson|Strings\resources.resjson\ko-KR\resources.resjson|Strings\resources.resjson\ru-RU\resources.resjson|Strings\resources.resjson\zh-CN\resources.resjson|Strings\resources.resjson\zh-TW\resources.resjson|InputFunctions.ps1|lib.json|LoggingCommandFunctions.ps1|Minimatch.dll|ServerOMFunctions.ps1|TraceFunctions.ps1|VstsTaskSdk.psm1|Strings\resources.resjson\en-US\resources.resjson|Strings\resources.resjson\fr-FR\resources.resjson - bbed04e2-4e8e-4089-90a2-58b858fed8d8 - 3.0 - Microsoft Corporation - - - D:\Work\GitHub\bcdevopsextension\temp\VstsTaskSdk\0.21.0 - - - diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 deleted file mode 100644 index 6fd19ea..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 +++ /dev/null @@ -1,684 +0,0 @@ -<# -.SYNOPSIS -Gets assembly reference information. - -.DESCRIPTION -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. - -Walks an assembly's references to determine all of it's dependencies. Also walks the references of the dependencies, and so on until all nested dependencies have been traversed. Dependencies are searched for in the directory of the specified assembly. NET Framework assemblies are omitted. - -See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER LiteralPath -Assembly to walk. - -.EXAMPLE -Get-VstsAssemblyReference -LiteralPath C:\nuget\microsoft.teamfoundationserver.client.14.102.0\lib\net45\Microsoft.TeamFoundation.Build2.WebApi.dll -#> -function Get-AssemblyReference { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$LiteralPath) - - $ErrorActionPreference = 'Stop' - 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) - $hashtable = @{ } - $queue = @( [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($LiteralPath).GetName() ) - while ($queue.Count) { - # Add a blank line between assemblies. - Write-Output '' - - # Pop. - $assemblyName = $queue[0] - $queue = @( $queue | Select-Object -Skip 1 ) - - # Attempt to find the assembly in the same directory. - $assembly = $null - $path = "$directory\$($assemblyName.Name).dll" - if ((Test-Path -LiteralPath $path -PathType Leaf)) { - $assembly = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($path) - } else { - $path = "$directory\$($assemblyName.Name).exe" - if ((Test-Path -LiteralPath $path -PathType Leaf)) { - $assembly = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($path) - } - } - - # Make sure the assembly full name matches, not just the file name. - if ($assembly -and $assembly.GetName().FullName -ne $assemblyName.FullName) { - $assembly = $null - } - - # Print the assembly. - if ($assembly) { - Write-Output $assemblyName.FullName - } else { - if ($assemblyName.FullName -eq 'Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed') { - Write-Warning "*** NOT FOUND $($assemblyName.FullName) *** This is an expected condition when using the HTTP clients from the 15.x VSTS REST SDK. Use Get-VstsVssHttpClient to load the HTTP clients (which applies a binding redirect assembly resolver for Newtonsoft.Json). Otherwise you will need to manage the binding redirect yourself." - } else { - Write-Warning "*** NOT FOUND $($assemblyName.FullName) ***" - } - - continue - } - - # Walk the references. - $refAssemblyNames = @( $assembly.GetReferencedAssemblies() ) - for ($i = 0 ; $i -lt $refAssemblyNames.Count ; $i++) { - $refAssemblyName = $refAssemblyNames[$i] - - # Skip framework assemblies. - $fxPaths = @( - "$env:windir\Microsoft.Net\Framework64\v4.0.30319\$($refAssemblyName.Name).dll" - "$env:windir\Microsoft.Net\Framework64\v4.0.30319\WPF\$($refAssemblyName.Name).dll" - ) - $fxPath = $fxPaths | - Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | - Where-Object { [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($_).GetName().FullName -eq $refAssemblyName.FullName } - if ($fxPath) { - continue - } - - # Print the reference. - Write-Output " $($refAssemblyName.FullName)" - - # Add new references to the queue. - if (!$hashtable[$refAssemblyName.FullName]) { - $queue += $refAssemblyName - $hashtable[$refAssemblyName.FullName] = $true - } - } - } -} - -<# -.SYNOPSIS -Gets a credentials object that can be used with the TFS extended client SDK. - -.DESCRIPTION -The agent job token is used 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). - -Refer to Get-VstsTfsService for a more simple to get a TFS service object. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER OMDirectory -Directory where the extended client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.EXAMPLE -# -# Refer to Get-VstsTfsService for a more simple way to get a TFS service object. -# -$credentials = Get-VstsTfsClientCredentials -Add-Type -LiteralPath "$PSScriptRoot\Microsoft.TeamFoundation.VersionControl.Client.dll" -$tfsTeamProjectCollection = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection( - (Get-VstsTaskVariable -Name 'System.TeamFoundationCollectionUri' -Require), - $credentials) -$versionControlServer = $tfsTeamProjectCollection.GetService([Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer]) -$versionControlServer.GetItems('$/*').Items | Format-List -#> -function Get-TfsClientCredentials { - [CmdletBinding()] - param([string]$OMDirectory) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Get the endpoint. - $endpoint = Get-Endpoint -Name SystemVssConnection -Require - - # Test if the Newtonsoft.Json DLL exists in the OM directory. - $newtonsoftDll = [System.IO.Path]::Combine($OMDirectory, "Newtonsoft.Json.dll") - Write-Verbose "Testing file path: '$newtonsoftDll'" - if (!(Test-Path -LiteralPath $newtonsoftDll -PathType Leaf)) { - Write-Verbose 'Not found. Rethrowing exception.' - throw - } - - # Add a binding redirect and try again. Parts of the Dev15 preview SDK have a - # 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] { - param($sender, $e) - - if ($e.Name -like 'Newtonsoft.Json, *') { - Write-Verbose "Resolving '$($e.Name)' to '$newtonsoftDll'." - - return [System.Reflection.Assembly]::LoadFrom($newtonsoftDll) - } - - return $null - } - [System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolve) - - # Validate the type can be found. - $null = Get-OMType -TypeName 'Microsoft.TeamFoundation.Client.TfsClientCredentials' -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require - - # Construct the credentials. - $credentials = New-Object Microsoft.TeamFoundation.Client.TfsClientCredentials($false) # Do not use default credentials. - $credentials.AllowInteractive = $false - $credentials.Federated = New-Object Microsoft.TeamFoundation.Client.OAuthTokenCredential([string]$endpoint.auth.parameters.AccessToken) - return $credentials - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Gets a TFS extended client service. - -.DESCRIPTION -Gets an instance of an ITfsTeamProjectCollectionObject. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER TypeName -Namespace-qualified type name of the service to get. - -.PARAMETER OMDirectory -Directory where the extended client object model DLLs are located. If the DLLs for the types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER Uri -URI to use when initializing the service. If not specified, defaults to System.TeamFoundationCollectionUri. - -.PARAMETER TfsClientCredentials -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 -$versionControlServer.GetItems('$/*').Items | Format-List -#> -function Get-TfsService { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TypeName, - - [string]$OMDirectory, - - [string]$Uri, - - $TfsClientCredentials) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Default the URI to the collection URI. - if (!$Uri) { - $Uri = Get-TaskVariable -Name System.TeamFoundationCollectionUri -Require - } - - # Default the credentials. - if (!$TfsClientCredentials) { - $TfsClientCredentials = Get-TfsClientCredentials -OMDirectory $OMDirectory - } - - # Validate the project collection type can be loaded. - $null = Get-OMType -TypeName 'Microsoft.TeamFoundation.Client.TfsTeamProjectCollection' -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require - - # Load the project collection object. - $tfsTeamProjectCollection = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection($Uri, $TfsClientCredentials) - - # Validate the requested type can be loaded. - $type = Get-OMType -TypeName $TypeName -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require - - # Return the service object. - return $tfsTeamProjectCollection.GetService($type) - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Gets a credentials object that can be used with the VSTS REST SDK. - -.DESCRIPTION -The agent job token is used 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 service build/release identity). - -Refer to Get-VstsVssHttpClient for a more simple to get a VSS HTTP client. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -.PARAMETER OMDirectory -Directory where the REST client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -.EXAMPLE -# -# Refer to Get-VstsTfsService for a more simple way to get a TFS service object. -# -# This example works using the 14.x .NET SDK. A Newtonsoft.Json 6.0 to 8.0 binding -# redirect may be required when working with the 15.x SDK. Or use Get-VstsVssHttpClient -# to avoid managing the binding redirect. -# -$vssCredentials = Get-VstsVssCredentials -$collectionUrl = New-Object System.Uri((Get-VstsTaskVariable -Name 'System.TeamFoundationCollectionUri' -Require)) -Add-Type -LiteralPath "$PSScriptRoot\Microsoft.TeamFoundation.Core.WebApi.dll" -$projectHttpClient = New-Object Microsoft.TeamFoundation.Core.WebApi.ProjectHttpClient($collectionUrl, $vssCredentials) -$projectHttpClient.GetProjects().Result -#> -function Get-VssCredentials { - [CmdletBinding()] - param([string]$OMDirectory) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Get the endpoint. - $endpoint = Get-Endpoint -Name SystemVssConnection -Require - - # Check if the VssOAuthAccessTokenCredential type is available. - if ((Get-OMType -TypeName 'Microsoft.VisualStudio.Services.OAuth.VssOAuthAccessTokenCredential' -OMKind 'WebApi' -OMDirectory $OMDirectory)) { - # Create the federated credential. - $federatedCredential = New-Object Microsoft.VisualStudio.Services.OAuth.VssOAuthAccessTokenCredential($endpoint.auth.parameters.AccessToken) - } else { - # Validate the fallback type can be loaded. - $null = Get-OMType -TypeName 'Microsoft.VisualStudio.Services.Client.VssOAuthCredential' -OMKind 'WebApi' -OMDirectory $OMDirectory -Require - - # Create the federated credential. - $federatedCredential = New-Object Microsoft.VisualStudio.Services.Client.VssOAuthCredential($endpoint.auth.parameters.AccessToken) - } - - # Return the credentials. - return New-Object Microsoft.VisualStudio.Services.Common.VssCredentials( - (New-Object Microsoft.VisualStudio.Services.Common.WindowsCredential($false)), # Do not use default credentials. - $federatedCredential, - [Microsoft.VisualStudio.Services.Common.CredentialPromptType]::DoNotPrompt) - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Gets a VSS HTTP client. - -.DESCRIPTION -Gets an instance of an VSS HTTP client. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -.PARAMETER TypeName -Namespace-qualified type name of the HTTP client to get. - -.PARAMETER OMDirectory -Directory where the REST client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -# .PARAMETER Uri -# URI to use when initializing the HTTP client. If not specified, defaults to System.TeamFoundationCollectionUri. - -# .PARAMETER VssCredentials -# 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 -$projectHttpClient.GetProjects().Result -#> -function Get-VssHttpClient { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TypeName, - - [string]$OMDirectory, - - [string]$Uri, - - $VssCredentials, - - $WebProxy = (Get-WebProxy), - - $ClientCert = (Get-ClientCertificate), - - [switch]$IgnoreSslError) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Default the URI to the collection URI. - if (!$Uri) { - $Uri = Get-TaskVariable -Name System.TeamFoundationCollectionUri -Require - } - - # Cast the URI. - [uri]$Uri = New-Object System.Uri($Uri) - - # Default the credentials. - if (!$VssCredentials) { - $VssCredentials = Get-VssCredentials -OMDirectory $OMDirectory - } - - # 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, $Settings) - } catch { - # Rethrow if the exception is not due to Newtonsoft.Json DLL not found. - if ($_.Exception.InnerException -isnot [System.IO.FileNotFoundException] -or - $_.Exception.InnerException.FileName -notlike 'Newtonsoft.Json, *') { - - throw - } - - # Default the OMDirectory to the directory of the entry script for the task. - if (!$OMDirectory) { - $OMDirectory = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") - Write-Verbose "Defaulted OM directory to: '$OMDirectory'" - } - - # Test if the Newtonsoft.Json DLL exists in the OM directory. - $newtonsoftDll = [System.IO.Path]::Combine($OMDirectory, "Newtonsoft.Json.dll") - Write-Verbose "Testing file path: '$newtonsoftDll'" - if (!(Test-Path -LiteralPath $newtonsoftDll -PathType Leaf)) { - Write-Verbose 'Not found. Rethrowing exception.' - throw - } - - # Add a binding redirect and try again. Parts of the Dev15 preview SDK have a - # 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] { - param($sender, $e) - - if ($e.Name -like 'Newtonsoft.Json, *') { - Write-Verbose "Resolving '$($e.Name)'" - return [System.Reflection.Assembly]::LoadFrom($newtonsoftDll) - } - - Write-Verbose "Unable to resolve assembly name '$($e.Name)'" - return $null - } - [System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolve) - try { - # Try again to construct the HTTP client. - Write-Verbose "Trying again to construct the HTTP client." - return New-Object $TypeName($Uri, $VssCredentials, $Settings) - } finally { - # Unregister the assembly resolver. - Write-Verbose "Removing assemlby resolver." - [System.AppDomain]::CurrentDomain.remove_AssemblyResolve($onAssemblyResolve) - } - } - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.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/azure-pipelines-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. -######################################## -function Get-OMType { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TypeName, - - [ValidateSet('ExtendedClient', 'WebApi')] - [Parameter(Mandatory = $true)] - [string]$OMKind, - - [string]$OMDirectory, - - [switch]$Require) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - try { - # Default the OMDirectory to the directory of the entry script for the task. - if (!$OMDirectory) { - $OMDirectory = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") - Write-Verbose "Defaulted OM directory to: '$OMDirectory'" - } - - # Try to load the type. - $errorRecord = $null - Write-Verbose "Testing whether type can be loaded: '$TypeName'" - $ErrorActionPreference = 'Ignore' - try { - # Failure when attempting to cast a string to a type, transfers control to the - # catch handler even when the error action preference is ignore. The error action - # is set to Ignore so the $Error variable is not polluted. - $type = [type]$TypeName - - # Success. - Write-Verbose "The type was loaded successfully." - return $type - } catch { - # Store the error record. - $errorRecord = $_ - } - - $ErrorActionPreference = 'Stop' - Write-Verbose "The type was not loaded." - - # Build a list of candidate DLL file paths from the namespace. - $dllPaths = @( ) - $namespace = $TypeName - while ($namespace.LastIndexOf('.') -gt 0) { - # Trim the next segment from the namespace. - $namespace = $namespace.SubString(0, $namespace.LastIndexOf('.')) - - # Derive potential DLL file paths based on the namespace and OM kind (i.e. extended client vs web API). - if ($OMKind -eq 'ExtendedClient') { - if ($namespace -like 'Microsoft.TeamFoundation.*') { - $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.dll") - } - } else { - if ($namespace -like 'Microsoft.TeamFoundation.*' -or - $namespace -like 'Microsoft.VisualStudio.Services' -or - $namespace -like 'Microsoft.VisualStudio.Services.*') { - - $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.WebApi.dll") - $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.dll") - } - } - } - - foreach ($dllPath in $dllPaths) { - # Check whether the DLL exists. - Write-Verbose "Testing leaf path: '$dllPath'" - if (!(Test-Path -PathType Leaf -LiteralPath "$dllPath")) { - Write-Verbose "Not found." - continue - } - - # Load the DLL. - Write-Verbose "Loading assembly: $dllPath" - try { - Add-Type -LiteralPath $dllPath - } catch { - # Write the information to the verbose stream and proceed to attempt to load the requested type. - # - # The requested type may successfully load now. For example, the type used with the 14.0 Web API for the - # federated credential (VssOAuthCredential) resides in Microsoft.VisualStudio.Services.Client.dll. Even - # though loading the DLL results in a ReflectionTypeLoadException when Microsoft.ServiceBus.dll (approx 3.75mb) - # is not present, enough types are loaded to use the VssOAuthCredential federated credential with the Web API - # HTTP clients. - Write-Verbose "$($_.Exception.GetType().FullName): $($_.Exception.Message)" - if ($_.Exception -is [System.Reflection.ReflectionTypeLoadException]) { - for ($i = 0 ; $i -lt $_.Exception.LoaderExceptions.Length ; $i++) { - $loaderException = $_.Exception.LoaderExceptions[$i] - Write-Verbose "LoaderExceptions[$i]: $($loaderException.GetType().FullName): $($loaderException.Message)" - } - } - } - - # Try to load the type. - Write-Verbose "Testing whether type can be loaded: '$TypeName'" - $ErrorActionPreference = 'Ignore' - try { - # Failure when attempting to cast a string to a type, transfers control to the - # catch handler even when the error action preference is ignore. The error action - # is set to Ignore so the $Error variable is not polluted. - $type = [type]$TypeName - - # Success. - Write-Verbose "The type was loaded successfully." - return $type - } catch { - $errorRecord = $_ - } - - $ErrorActionPreference = 'Stop' - Write-Verbose "The type was not loaded." - } - - # Check whether to propagate the error. - if ($Require) { - Write-Error $errorRecord - } - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson deleted file mode 100644 index 248b674..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Fehler beim Aufzählen von Unterverzeichnissen für den folgenden Pfad: \"{0}\"", - "loc.messages.PSLIB_FileNotFound0": "Die Datei wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_Input0": "\"{0}\"-Eingabe", - "loc.messages.PSLIB_InvalidPattern0": "Ungültiges Muster: \"{0}\"", - "loc.messages.PSLIB_LeafPathNotFound0": "Der Blattpfad wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Fehler bei der Normalisierung bzw. Erweiterung des Pfads. Die Pfadlänge wurde vom Kernel32-Subsystem nicht zurückgegeben für: \"{0}\"", - "loc.messages.PSLIB_PathNotFound0": "Der Pfad wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Der Prozess \"{0}\" wurde mit dem Code \"{1}\" beendet.", - "loc.messages.PSLIB_Required0": "Erforderlich: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Fehler beim Zeichenfolgenformat.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "Der Zeichenfolgen-Ressourcenschlüssel wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_TaskVariable0": "\"{0}\"-Taskvariable" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson deleted file mode 100644 index 66c17bc..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Enumerating subdirectories failed for path: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "File not found: '{0}'", - "loc.messages.PSLIB_Input0": "'{0}' input", - "loc.messages.PSLIB_InvalidPattern0": "Invalid pattern: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Leaf path not found: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Path normalization/expansion failed. The path length was not returned by the Kernel32 subsystem for: '{0}'", - "loc.messages.PSLIB_PathNotFound0": "Path not found: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Process '{0}' exited with code '{1}'.", - "loc.messages.PSLIB_Required0": "Required: {0}", - "loc.messages.PSLIB_StringFormatFailed": "String format failed.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "String resource key not found: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' task variable" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson deleted file mode 100644 index b79ac21..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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}'", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "No se pudieron enumerar los subdirectorios de la ruta de acceso: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "Archivo no encontrado: '{0}'", - "loc.messages.PSLIB_Input0": "Entrada '{0}'", - "loc.messages.PSLIB_InvalidPattern0": "Patrón no válido: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "No se encuentra la ruta de acceso de la hoja: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "No se pudo normalizar o expandir la ruta de acceso. El subsistema Kernel32 no devolvió la longitud de la ruta de acceso para: '{0}'", - "loc.messages.PSLIB_PathNotFound0": "No se encuentra la ruta de acceso: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "El proceso '{0}' finalizó con el código '{1}'.", - "loc.messages.PSLIB_Required0": "Se requiere: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Error de formato de cadena.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "No se encuentra la clave de recurso de la cadena: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "Variable de tarea '{0}'" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson deleted file mode 100644 index dc2da05..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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}'", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Échec de l'énumération des sous-répertoires pour le chemin : '{0}'", - "loc.messages.PSLIB_FileNotFound0": "Fichier introuvable : {0}.", - "loc.messages.PSLIB_Input0": "Entrée '{0}'", - "loc.messages.PSLIB_InvalidPattern0": "Modèle non valide : '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Le chemin feuille est introuvable : '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Échec de la normalisation/l'expansion du chemin. La longueur du chemin n'a pas été retournée par le sous-système Kernel32 pour : '{0}'", - "loc.messages.PSLIB_PathNotFound0": "Chemin introuvable : '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Le processus '{0}' s'est arrêté avec le code '{1}'.", - "loc.messages.PSLIB_Required0": "Obligatoire : {0}", - "loc.messages.PSLIB_StringFormatFailed": "Échec du format de la chaîne.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "Clé de la ressource de type chaîne introuvable : '{0}'", - "loc.messages.PSLIB_TaskVariable0": "Variable de tâche '{0}'" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson deleted file mode 100644 index 77bea53..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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}'", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "L'enumerazione delle sottodirectory per il percorso '{0}' non è riuscita", - "loc.messages.PSLIB_FileNotFound0": "File non trovato: '{0}'", - "loc.messages.PSLIB_Input0": "Input di '{0}'", - "loc.messages.PSLIB_InvalidPattern0": "Criterio non valido: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Percorso foglia non trovato: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "La normalizzazione o l'espansione del percorso non è riuscita. Il sottosistema Kernel32 non ha restituito la lunghezza del percorso per '{0}'", - "loc.messages.PSLIB_PathNotFound0": "Percorso non trovato: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Il processo '{0}' è stato terminato ed è stato restituito il codice '{1}'.", - "loc.messages.PSLIB_Required0": "Obbligatorio: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Errore nel formato della stringa.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "La chiave della risorsa stringa non è stata trovata: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "Variabile dell'attività '{0}'" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson deleted file mode 100644 index 9f2f9fe..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "バージョン {0} 以降のエージェントが必要です。", - "loc.messages.PSLIB_ContainerPathNotFound0": "コンテナーのパスが見つかりません: '{0}'", - "loc.messages.PSLIB_EndpointAuth0": "'{0}' サービス エンドポイントの資格情報", - "loc.messages.PSLIB_EndpointUrl0": "'{0}' サービス エンドポイントの URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "パス '{0}' のサブディレクトリを列挙できませんでした", - "loc.messages.PSLIB_FileNotFound0": "ファイルが見つかりません: '{0}'", - "loc.messages.PSLIB_Input0": "'{0}' 入力", - "loc.messages.PSLIB_InvalidPattern0": "使用できないパターンです: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "リーフ パスが見つかりません: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "パスの正規化/展開に失敗しました。Kernel32 サブシステムからパス '{0}' の長さが返されませんでした", - "loc.messages.PSLIB_PathNotFound0": "パスが見つかりません: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "プロセス '{0}' がコード '{1}' で終了しました。", - "loc.messages.PSLIB_Required0": "必要: {0}", - "loc.messages.PSLIB_StringFormatFailed": "文字列のフォーマットに失敗しました。", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "文字列のリソース キーが見つかりません: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' タスク変数" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson deleted file mode 100644 index d809a2e..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "에이전트 버전 {0} 이상이 필요합니다.", - "loc.messages.PSLIB_ContainerPathNotFound0": "컨테이너 경로를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_EndpointAuth0": "'{0}' 서비스 엔드포인트 자격 증명", - "loc.messages.PSLIB_EndpointUrl0": "'{0}' 서비스 엔드포인트 URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "경로에 대해 하위 디렉터리를 열거하지 못함: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "{0} 파일을 찾을 수 없습니다.", - "loc.messages.PSLIB_Input0": "'{0}' 입력", - "loc.messages.PSLIB_InvalidPattern0": "잘못된 패턴: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Leaf 경로를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "경로 정규화/확장에 실패했습니다. 다음에 대해 Kernel32 subsystem에서 경로 길이를 반환하지 않음: '{0}'", - "loc.messages.PSLIB_PathNotFound0": "경로를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "'{1}' 코드로 '{0}' 프로세스가 종료되었습니다.", - "loc.messages.PSLIB_Required0": "필수: {0}", - "loc.messages.PSLIB_StringFormatFailed": "문자열을 포맷하지 못했습니다.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "문자열 리소스 키를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' 작업 변수" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson deleted file mode 100644 index 40daae7..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "Требуется версия агента {0} или более поздняя.", - "loc.messages.PSLIB_ContainerPathNotFound0": "Путь к контейнеру не найден: \"{0}\".", - "loc.messages.PSLIB_EndpointAuth0": "Учетные данные конечной точки службы \"{0}\"", - "loc.messages.PSLIB_EndpointUrl0": "URL-адрес конечной точки службы \"{0}\"", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Сбой перечисления подкаталогов для пути: \"{0}\".", - "loc.messages.PSLIB_FileNotFound0": "Файл не найден: \"{0}\"", - "loc.messages.PSLIB_Input0": "Входные данные \"{0}\".", - "loc.messages.PSLIB_InvalidPattern0": "Недопустимый шаблон: \"{0}\".", - "loc.messages.PSLIB_LeafPathNotFound0": "Путь к конечному объекту не найден: \"{0}\".", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Сбой нормализации и расширения пути. Длина пути не была возвращена подсистемой Kernel32 для: \"{0}\".", - "loc.messages.PSLIB_PathNotFound0": "Путь не найден: \"{0}\".", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Процесс \"{0}\" завершил работу с кодом \"{1}\".", - "loc.messages.PSLIB_Required0": "Требуется: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Сбой формата строки.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "Ключ ресурса строки не найден: \"{0}\".", - "loc.messages.PSLIB_TaskVariable0": "Переменная задачи \"{0}\"" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson deleted file mode 100644 index 49d824b..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "需要代理版本 {0} 或更高版本。", - "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路径:“{0}”", - "loc.messages.PSLIB_EndpointAuth0": "“{0}”服务终结点凭据", - "loc.messages.PSLIB_EndpointUrl0": "“{0}”服务终结点 URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "枚举路径的子目录失败:“{0}”", - "loc.messages.PSLIB_FileNotFound0": "找不到文件: {0}。", - "loc.messages.PSLIB_Input0": "“{0}”输入", - "loc.messages.PSLIB_InvalidPattern0": "无效的模式:“{0}”", - "loc.messages.PSLIB_LeafPathNotFound0": "找不到叶路径:“{0}”", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "路径规范化/扩展失败。路径长度不是由“{0}”的 Kernel32 子系统返回的", - "loc.messages.PSLIB_PathNotFound0": "找不到路径:“{0}”", - "loc.messages.PSLIB_Process0ExitedWithCode1": "过程“{0}”已退出,代码为“{1}”。", - "loc.messages.PSLIB_Required0": "必需: {0}", - "loc.messages.PSLIB_StringFormatFailed": "字符串格式无效。", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "找不到字符串资源关键字:“{0}”", - "loc.messages.PSLIB_TaskVariable0": "“{0}”任务变量" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson deleted file mode 100644 index 7cbf22e..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "需要代理程式版本 {0} 或更新的版本。", - "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路徑: '{0}'", - "loc.messages.PSLIB_EndpointAuth0": "'{0}' 服務端點認證", - "loc.messages.PSLIB_EndpointUrl0": "'{0}' 服務端點 URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "為路徑列舉子目錄失敗: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "找不到檔案: '{0}'", - "loc.messages.PSLIB_Input0": "'{0}' 輸入", - "loc.messages.PSLIB_InvalidPattern0": "模式無效: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "找不到分葉路徑: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "路徑正規化/展開失敗。Kernel32 子系統未傳回 '{0}' 的路徑長度", - "loc.messages.PSLIB_PathNotFound0": "找不到路徑: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "處理序 '{0}' 以返回碼 '{1}' 結束。", - "loc.messages.PSLIB_Required0": "必要項: {0}", - "loc.messages.PSLIB_StringFormatFailed": "字串格式失敗。", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "找不到字串資源索引鍵: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' 工作變數" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/ToolFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/ToolFunctions.ps1 deleted file mode 100644 index bb6f8d6..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/ToolFunctions.ps1 +++ /dev/null @@ -1,227 +0,0 @@ -<# -.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. - -.PARAMETER PassThru -True to return the path. -#> -function Assert-Path { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$LiteralPath, - [Microsoft.PowerShell.Commands.TestPathType]$PathType = [Microsoft.PowerShell.Commands.TestPathType]::Any, - [switch]$PassThru) - - if ($PathType -eq [Microsoft.PowerShell.Commands.TestPathType]::Any) { - Write-Verbose "Asserting path exists: '$LiteralPath'" - } - else { - Write-Verbose "Asserting $("$PathType".ToLowerInvariant()) path exists: '$LiteralPath'" - } - - if (Test-Path -LiteralPath $LiteralPath -PathType $PathType) { - if ($PassThru) { - return $LiteralPath - } - - return - } - - $resourceKey = switch ($PathType) { - ([Microsoft.PowerShell.Commands.TestPathType]::Container) { "PSLIB_ContainerPathNotFound0" ; break } - ([Microsoft.PowerShell.Commands.TestPathType]::Leaf) { "PSLIB_LeafPathNotFound0" ; break } - default { "PSLIB_PathNotFound0" } - } - - throw (Get-LocString -Key $resourceKey -ArgumentList $LiteralPath) -} - -<# -.SYNOPSIS -Executes an external program. - -.DESCRIPTION -Executes an external program and waits for the process to exit. - -After calling this command, the exit code of the process can be retrieved from the variable $LASTEXITCODE. - -.PARAMETER Encoding -This parameter not required for most scenarios. Indicates how to interpret the encoding from the external program. An example use case would be if an external program outputs UTF-16 XML and the output needs to be parsed. - -.PARAMETER RequireExitCodeZero -Indicates whether to write an error to the error pipeline if the exit code is not zero. -#> -function Invoke-Tool { - [CmdletBinding()] - param( - [ValidatePattern('^[^\r\n]*$')] - [Parameter(Mandatory = $true)] - [string]$FileName, - [ValidatePattern('^[^\r\n]*$')] - [Parameter()] - [string]$Arguments, - [string]$WorkingDirectory, - [System.Text.Encoding]$Encoding, - [switch]$RequireExitCodeZero, - [bool]$IgnoreHostException) - - Trace-EnteringInvocation $MyInvocation - $isPushed = $false - $originalEncoding = $null - try { - if ($Encoding) { - $originalEncoding = [System.Console]::OutputEncoding - [System.Console]::OutputEncoding = $Encoding - } - - if ($WorkingDirectory) { - Push-Location -LiteralPath $WorkingDirectory -ErrorAction Stop - $isPushed = $true - } - - $FileName = $FileName.Replace('"', '').Replace("'", "''") - Write-Host "##[command]""$FileName"" $Arguments" - try { - Invoke-Expression "& '$FileName' --% $Arguments" - } - catch [System.Management.Automation.Host.HostException] { - if ($IgnoreHostException -eq $False) { - throw - } - - Write-Host "##[warning]Host Exception was thrown by Invoke-Expression, suppress it due IgnoreHostException setting" - } - Write-Verbose "Exit code: $LASTEXITCODE" - if ($RequireExitCodeZero -and $LASTEXITCODE -ne 0) { - Write-Error (Get-LocString -Key PSLIB_Process0ExitedWithCode1 -ArgumentList ([System.IO.Path]::GetFileName($FileName)), $LASTEXITCODE) - } - } - finally { - if ($originalEncoding) { - [System.Console]::OutputEncoding = $originalEncoding - } - - if ($isPushed) { - Pop-Location - } - - Trace-LeavingInvocation $MyInvocation - } -} - -<# -.SYNOPSIS -Executes an external program as a child process. - -.DESCRIPTION -Executes an external program and waits for the process to exit. - -After calling this command, the exit code of the process can be retrieved from the variable $LASTEXITCODE or from the pipe. - -.PARAMETER FileName -File name (path) of the program to execute. - -.PARAMETER Arguments -Arguments to pass to the program. - -.PARAMETER StdOutPath -Path to a file to write the stdout of the process to. - -.PARAMETER StdErrPath -Path to a file to write the stderr of the process to. - -.PARAMETER RequireExitCodeZero -Indicates whether to write an error to the error pipeline if the exit code is not zero. - -.OUTPUTS -Exit code of the invoked process. Also available through the $LASTEXITCODE. - -.NOTES -To change output encoding, redirect stdout to file and then read the file with the desired encoding. -#> -function Invoke-Process { - [CmdletBinding()] - param( - [ValidatePattern('^[^\r\n]*$')] - [Parameter(Mandatory = $true)] - [string]$FileName, - [ValidatePattern('^[^\r\n]*$')] - [Parameter()] - [string]$Arguments, - [string]$WorkingDirectory, - [string]$StdOutPath, - [string]$StdErrPath, - [switch]$RequireExitCodeZero - ) - - Trace-EnteringInvocation $MyInvocation - try { - $FileName = $FileName.Replace('"', '').Replace("'", "''") - Write-Host "##[command]""$FileName"" $Arguments" - - $processOptions = @{ - FilePath = $FileName - NoNewWindow = $true - PassThru = $true - } - if ($Arguments) { - $processOptions.Add("ArgumentList", $Arguments) - } - if ($WorkingDirectory) { - $processOptions.Add("WorkingDirectory", $WorkingDirectory) - } - if ($StdOutPath) { - $processOptions.Add("RedirectStandardOutput", $StdOutPath) - } - if ($StdErrPath) { - $processOptions.Add("RedirectStandardError", $StdErrPath) - } - - # TODO: For some reason, -Wait is not working on agent. - # Agent starts executing the System usage metrics and hangs the step forever. - $proc = Start-Process @processOptions - - # https://stackoverflow.com/a/23797762 - $null = $($proc.Handle) - $proc.WaitForExit() - - $procExitCode = $proc.ExitCode - Write-Verbose "Exit code: $procExitCode" - - if ($RequireExitCodeZero -and $procExitCode -ne 0) { - Write-Error (Get-LocString -Key PSLIB_Process0ExitedWithCode1 -ArgumentList ([System.IO.Path]::GetFileName($FileName)), $procExitCode) - } - - $global:LASTEXITCODE = $procExitCode - - return $procExitCode - } - finally { - Trace-LeavingInvocation $MyInvocation - } -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/TraceFunctions.ps1 b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/TraceFunctions.ps1 deleted file mode 100644 index cdbd779..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/TraceFunctions.ps1 +++ /dev/null @@ -1,139 +0,0 @@ -<# -.SYNOPSIS -Writes verbose information about the invocation being entered. - -.DESCRIPTION -Used to trace verbose information when entering a function/script. Writes an entering message followed by a short description of the invocation. Additionally each bound parameter and unbound argument is also traced. - -.PARAMETER Parameter -Wildcard pattern to control which bound parameters are traced. -#> -function Trace-EnteringInvocation { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [System.Management.Automation.InvocationInfo]$InvocationInfo, - [string[]]$Parameter = '*') - - Write-Verbose "Entering $(Get-InvocationDescription $InvocationInfo)." - $OFS = ", " - if ($InvocationInfo.BoundParameters.Count -and $Parameter.Count) { - if ($Parameter.Count -eq 1 -and $Parameter[0] -eq '*') { - # Trace all parameters. - foreach ($key in $InvocationInfo.BoundParameters.Keys) { - Write-Verbose " $($key): '$($InvocationInfo.BoundParameters[$key])'" - } - } else { - # Trace matching parameters. - foreach ($key in $InvocationInfo.BoundParameters.Keys) { - foreach ($p in $Parameter) { - if ($key -like $p) { - Write-Verbose " $($key): '$($InvocationInfo.BoundParameters[$key])'" - break - } - } - } - } - } - - # Trace all unbound arguments. - if (@($InvocationInfo.UnboundArguments).Count) { - for ($i = 0 ; $i -lt $InvocationInfo.UnboundArguments.Count ; $i++) { - Write-Verbose " args[$i]: '$($InvocationInfo.UnboundArguments[$i])'" - } - } -} - -<# -.SYNOPSIS -Writes verbose information about the invocation being left. - -.DESCRIPTION -Used to trace verbose information when leaving a function/script. Writes a leaving message followed by a short description of the invocation. -#> -function Trace-LeavingInvocation { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [System.Management.Automation.InvocationInfo]$InvocationInfo) - - Write-Verbose "Leaving $(Get-InvocationDescription $InvocationInfo)." -} - -<# -.SYNOPSIS -Writes verbose information about paths. - -.DESCRIPTION -Writes verbose information about the paths. The paths are sorted and a the common root is written only once, followed by each relative path. - -.PARAMETER PassThru -Indicates whether to return the sorted paths. -#> -function Trace-Path { - [CmdletBinding()] - param( - [string[]]$Path, - [switch]$PassThru) - - if ($Path.Count -eq 0) { - Write-Verbose "No paths." - if ($PassThru) { - $Path - } - } elseif ($Path.Count -eq 1) { - Write-Verbose "Path: $($Path[0])" - if ($PassThru) { - $Path - } - } else { - # Find the greatest common root. - $sorted = $Path | Sort-Object - $firstPath = $sorted[0].ToCharArray() - $lastPath = $sorted[-1].ToCharArray() - $commonEndIndex = 0 - $j = if ($firstPath.Length -lt $lastPath.Length) { $firstPath.Length } else { $lastPath.Length } - for ($i = 0 ; $i -lt $j ; $i++) { - if ($firstPath[$i] -eq $lastPath[$i]) { - if ($firstPath[$i] -eq '\') { - $commonEndIndex = $i - } - } else { - break - } - } - - if ($commonEndIndex -eq 0) { - # No common root. - Write-Verbose "Paths:" - foreach ($p in $sorted) { - Write-Verbose " $p" - } - } else { - Write-Verbose "Paths: $($Path[0].Substring(0, $commonEndIndex + 1))" - foreach ($p in $sorted) { - Write-Verbose " $($p.Substring($commonEndIndex + 1))" - } - } - - if ($PassThru) { - $sorted - } - } -} - -######################################## -# Private functions. -######################################## -function Get-InvocationDescription { - [CmdletBinding()] - param([System.Management.Automation.InvocationInfo]$InvocationInfo) - - if ($InvocationInfo.MyCommand.Path) { - $InvocationInfo.MyCommand.Path - } elseif ($InvocationInfo.MyCommand.Name) { - $InvocationInfo.MyCommand.Name - } else { - $InvocationInfo.MyCommand.CommandType - } -} diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/VstsTaskSdk.dll b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/VstsTaskSdk.dll deleted file mode 100644 index 54938ab05180ddaf77cf067f2501a1f97ee7e5cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25408 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

8wuJ@>HyfDZ;=w9&`lhU;c$=Kxs zVyl3=oCzOR%(5JON~b1f_qN+!&dWl_U8}fsV#Ix3YxI$et=Qqtrbj{FWE$c^p03gw zvH`3R`Cpf{D%4sIAA!^?|@!O&A+cc|Ex;y|6?0F+wS9quHW9^=zrzn|8Li++DDT^mzVoUZBHf{%(A&` z+&F2uZRr5BJJx3?7i7uTq+cCqJ8j`#b-DS*ZPDce-qXtB9+EzMH(i&wvaNu;{++#% zcYtkc-G2VJ;sr0;Hp{x<5&6O@^OK@zp)En2H5kd8W?t#(tXU5Z@fTX(n%L1~H8J*xd0XA~tGY9FO!V{GAFRD#G3e@uMn47pstt`F{7#1- zWi49pHQ5e*sle1z<+k#Y0!F=g&RZiY9=|6+d&$Bv^Oy%)?#h}hI33WlP7MZ5srI`8 zDs#%WStH@>)5B>?n&RoQiFMb_FJ&u2u-$bQcf-@rv^-_J?e9(`T1x!=l656Xj>}^1^N?Y zEH15`x&O7D>6gB*9@02r$cOH;!{#ET_rTm5&V**TKN>0b;r!$&7 zOAU12dChrl?rV^B`RJpSS7)tK4q5!9fO%flyo3_*&h4K1ZC-4k&b8r6MwZ@JiaM)~ z+|?P{_g#wv|hI+^_}a5)~dOiu6HO_wO15Ta=@hEB<@82bDt@uz~NpDL_$1jV})P zncLo2a%j)IPX-%V{8`iIS(O(a!3qym6&}C}>;5B#_pd(&`QvX6gCiq|-yB-m+1Z-g zcv}0Id)j(=vrIALE_+?~S9_fqA;=PPxXd8|_?4dG}ryA~vq&3``QwMp3z+P|*{XsQBd)PR}pjq?*aeSuZZ! zRSve}Uh-LgC!#g_l=p1Kmv>pm+V?CrcJmz8xH#o_dB(OIMI$FLj3nmd>fAl(#7&wb zecG^fjm+4KckiE270=kN8Ii*_)Ob2PZJaG-S^BkBlM>gxoZV8x3k$bA^TtGY z@7tt9+x9W*H5SEK2-lwObj0@R961rZVGYTEzo-!}zCMc?y&%O1 zG7Ij;b)@+gJzBYZ`i5}x{cW+|S6l2U74)C1u;}1diVEIM-19nj@yVoFe~6!l z%wh{Uc5cN6D|N&D&*mS#H~7>BFH=p|<6jsSG3Qc?atAbZls%Lx^Y^#hYTvSsYPg&f zaAuI>jn3V3Xt^=l;%HyeiyFg8QdckPyCeg8Z=(>)u$VSSVt;j6wbe_ zw6|WRHhZ|_nuYUYyjeTqB4-qOB%91~SKi&KH{s{*;caH?8)~j|wf1CA91}&4Q(dOr ze1vf_*<|*suxPoIQ~R85&NbT^@0+1{t#L-gHs8oM&1 | - ForEach-Object { - if (`$_ -is [System.Management.Automation.ErrorRecord]) { - Write-Verbose `$_.Exception.Message - } else { - ,`$_ - } - } -} -"@ -. ([scriptblock]::Create($scriptText)) 2>&1 3>&1 4>&1 5>&1 | Out-Default - -# Create Invoke-VstsTaskScript in a special way so it is not bound to the module. -# Otherwise calling the task script block would run within the module context. -# -# An alternative way to solve the problem is to close the script block (i.e. closure). -# However, that introduces a different problem. Closed script blocks are created within -# a dynamic module. Each module gets it's own session state separate from the global -# session state. When running in a regular script context, Import-Module calls import -# the target module into the global session state. When running in a module context, -# Import-Module calls import the target module into the caller module's session state. -# -# The goal of a task may include executing ad-hoc scripts. Therefore, task scripts -# should run in regular script context. The end user specifying an ad-hoc script expects -# the module import rules to be consistent with the default behavior (i.e. imported -# into the global session state). -$null = New-Item -Force -Path "function:\global:Invoke-VstsTaskScript" -Value ([scriptblock]::Create(@' - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [scriptblock]$ScriptBlock) - - try { - $global:ErrorActionPreference = 'Stop' - - # Initialize the environment. - $vstsModule = Get-Module -Name VstsTaskSdk - Write-Verbose "$($vstsModule.Name) $($vstsModule.Version) commit $($vstsModule.PrivateData.PSData.CommitHash)" 4>&1 | Out-Default - & $vstsModule Initialize-Inputs 4>&1 | Out-Default - - # Remove the local variable before calling the user's script. - Remove-Variable -Name vstsModule - - # Call the user's script. - $ScriptBlock | - ForEach-Object { - # Remove the scriptblock variable before calling it. - Remove-Variable -Name ScriptBlock - & $_ 2>&1 3>&1 4>&1 5>&1 | Out-Default - } - } catch [VstsTaskSdk.TerminationException] { - # Special internal exception type to control the flow. Not currently intended - # for public usage and subject to change. - $global:__vstsNoOverrideVerbose = '' - Write-Verbose "Task script terminated." 4>&1 | Out-Default - } catch { - $global:__vstsNoOverrideVerbose = '' - Write-Verbose "Caught exception from task script." 4>&1 | Out-Default - $_ | Out-Default - Write-Host "##vso[task.complete result=Failed]" - } -'@)) diff --git a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/lib.json b/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/lib.json deleted file mode 100644 index 0cde160..0000000 --- a/bc-tools-extension/Get-BCDependencies/ps_modules/VstsTaskSdk/lib.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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", - "PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Enumerating subdirectories failed for path: '{0}'", - "PSLIB_FileNotFound0": "File not found: '{0}'", - "PSLIB_Input0": "'{0}' input", - "PSLIB_InvalidPattern0": "Invalid pattern: '{0}'", - "PSLIB_LeafPathNotFound0": "Leaf path not found: '{0}'", - "PSLIB_PathLengthNotReturnedFor0": "Path normalization/expansion failed. The path length was not returned by the Kernel32 subsystem for: '{0}'", - "PSLIB_PathNotFound0": "Path not found: '{0}'", - "PSLIB_Process0ExitedWithCode1": "Process '{0}' exited with code '{1}'.", - "PSLIB_Required0": "Required: {0}", - "PSLIB_StringFormatFailed": "String format failed.", - "PSLIB_StringResourceKeyNotFound0": "String resource key not found: '{0}'", - "PSLIB_TaskVariable0": "'{0}' task variable" - } -} diff --git a/bc-tools-extension/Get-BCDependencies/task.json b/bc-tools-extension/Get-BCDependencies/task.json index 1430f76..228899a 100644 --- a/bc-tools-extension/Get-BCDependencies/task.json +++ b/bc-tools-extension/Get-BCDependencies/task.json @@ -84,9 +84,6 @@ }, "Node20_1": { "target": "function_Get-BCDependencies.js" - }, - "PowerShell3": { - "target": "wrapper_Get-BCDependencies.ps1" } } } diff --git a/bc-tools-extension/Get-BCDependencies/wrapper_Get-BCDependencies.ps1 b/bc-tools-extension/Get-BCDependencies/wrapper_Get-BCDependencies.ps1 deleted file mode 100644 index 3f066d5..0000000 --- a/bc-tools-extension/Get-BCDependencies/wrapper_Get-BCDependencies.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -. "./function_Add-BaseDependenciesIfMissing.ps1" -. "./function_Get-BCDependencies.ps1" - -$local_TestLoginOnly = Get-VstsInput -Name 'TestLoginOnly' -$local_SkipDefaultDependencies = Get-VstsInput -Name 'SkipDefaultDependencies' -$local_TenantId = Get-VstsInput -Name 'TenantId' -$local_EnvironmentName = Get-VstsInput -Name 'EnvironmentName' -$local_ClientId = Get-VstsInput -Name 'ClientId' -$local_ClientSecret = Get-VstsInput -Name 'ClientSecret' -$local_PathToAppJson = Get-VstsInput -Name 'PathToAppJson' -$local_PathToPackagesDirectory = Get-VstsInput -Name 'PathToPackagesDirectory' - -$switchParams = @{} - -if ($local_TestLoginOnly -eq 'true') { - $switchParams["TestLoginOnly"] = $true -} - -if ($local_SkipDefaultDependencies -eq 'true') { - $switchParams["SkipDefaultDependencies"] = $true -} - -$switchParams["TenantId"] = $local_TenantId -$switchParams["EnvironmentName"] = $local_EnvironmentName -$switchParams["ClientId"] = $local_ClientId -$switchParams["ClientSecret"] = $local_ClientSecret -$switchParams["PathToAppJson"] = $local_PathToAppJson -$switchParams["PathToPackagesDirectory"] = $local_PathToPackagesDirectory - -Write-Host "Getting AL Dependencies:" -$switchParams.GetEnumerator() | ForEach-Object { - if ($_.Key -eq "ClientSecret") { - Write-Host (" {0,-30} = {1}" -f $_.Key, "Are you nuts?") - } - else { - Write-Host (" {0,-30} = {1}" -f $_.Key, $_.Value) - } -} - -Get-BCDependencies @switchParams \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 deleted file mode 100644 index 928edde..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.ps1 +++ /dev/null @@ -1,214 +0,0 @@ -function Get-VSIXCompilerVersion { - param( - [Parameter(Mandatory)] - [String]$Version, - [Parameter(Mandatory)] - [String]$DownloadDirectory, - [Parameter()] - [Switch]$DebugMode - ) - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $ProgressPreference = 'SilentlyContinue' - - Write-Host "Determining platform" - if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - $platform = "win32" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - $platform = "win32" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - $platform = "linux" - } - else { - Write-Error "Unsupported platform: $([System.Runtime.InteropServices.RuntimeInformation]::OSDescription)" - exit 1 - } - - Write-Host "Detected platform: $platform" - - $apiUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" - - Write-Host "Contacting '$apiUrl'" - - $jsonRawPrototype = [ordered]@{ - filters = @( - @{ - criteria = @( - @{ - filterType = 7 - value = 'ms-dynamics-smb.al' - } - ) - pageNumber = 1 - pageSize = 100 - sortBy = 0 - sortOrder = 0 - } - ) - assetTypes = @() - flags = 129 - } | ConvertTo-Json -Depth 10 - - try { - $headers = @{"Accept" = "application/json; charset=utf-8;api-version=7.2-preview.1" } - $restResult = Invoke-RestMethod -Uri $apiUrl -Method Post -Body $jsonRawPrototype -ContentType "application/json" -Headers $headers - } - catch { - Write-Error "An error occurred: $($_.Exception.Message)" - exit 1 - } - - if (-not $restResult) { - Write-Error "Something went wrong, didn't get a proper response from the API" - exit 1 - } - - Write-Host "Received response from the API with $($restResult.results[0].extensions[0].versions.Count) versions coming back" - - $publisher = $restResult.results[0].extensions[0].publisher.publisherName - $extension = $restResult.results[0].extensions[0].ExtensionName - - if ($Version -eq 'latest') { - $getVersion = $restResult.results[0].extensions[0].versions[0].version - } - else { - $versions = $restResult.results[0].extensions[0].versions - $versionExists = $versions.version -contains $Version - if ($versionExists) { - $getVersion = $Version - } - else { - Write-Error "Version $Version was not found in the list of versions; please check your version number or try 'latest'" - exit 1 - } - } - - Write-Host "Acquiring compiler with the following metadata:" - Write-Host (" {0,-20} = {1}" -f "publisher", $publisher) - Write-Host (" {0,-20} = {1}" -f "extension", $extension) - Write-Host (" {0,-20} = {1}" -f "version", $version) - Write-Host "" - - $downloadUrl = "https://$($publisher).gallery.vsassets.io/_apis/public/gallery/publisher/$($publisher)/extension/$($extension)/$($getVersion)/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage" - Write-Host "Acquisition: $downloadUrl" - - if (-not (Test-Path -Path $DownloadDirectory)) { - New-Item -ItemType Directory -Path $DownloadDirectory - Write-Host "Creating directory: $DownloadDirectory" - } - - $target = Join-Path -Path $DownloadDirectory -ChildPath "compiler.vsix" - Write-Host "Download target: $target" - - if (-not $DebugMode) { - Invoke-WebRequest -Uri $downloadUrl -OutFile $target -UseBasicParsing - Write-Host "Downloaded file: $target" - $originalHash = Get-FileHash -Path $target -Algorithm SHA256 - Write-Host "SHA256: $($originalHash.Hash)" - } - - # Step 3: Rename file because Azure Pipelines' version of Expand-Archive is a little b**** - $newFileName = "compiler.zip" - $newPath = Join-Path -Path (Split-Path $target) -ChildPath $newFileName - Rename-Item -Path $target -NewName $newPath -Force - - Write-Host "Renamed '$target' to '$newFileName' for unzipping" - - if (-not (Test-Path -Path $newPath)) { - Write-Host "Hey! compiler.zip rename didn't work; I'll try copying it instead" - Copy-Item -Path $target -Destination $newPath -Force - Write-Host "I just copied from $target to $newPath" - } else { - Write-Host "Just confirmed there is a path (file) at $newPath" - } - - Write-Host "########################################################################################################################################" - Write-Host "Checking on .zip file status of file '$newPath'" - Write-Host "########################################################################################################################################" - Write-Host "" - - $fileSize = (Get-Item -Path $newPath).Length - Write-Host "File size: $fileSize bytes" - - $fileHash = Get-FileHash -Path $newPath -Algorithm SHA256 - Write-Host "SHA256 [new]: $($fileHash.Hash)" - Write-Host "SHA256 [old]: $($originalHash.Hash)" - - $bytes = Get-Content -Path $newPath -Encoding Byte -TotalCount 4 - if ($bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B) { - Write-Host "The file header adheres to a PK file header" - } else { - throw "The resulting file at $newPath does not appear to have a PK header" - } - - Write-Host "" - Write-Host "########################################################################################################################################" - - $expandFolder = Join-Path -Path $DownloadDirectory -ChildPath "expanded" - - if (-not (Test-Path -Path $expandFolder)) { - New-Item -ItemType Directory -Path $expandFolder - Write-Host "Created folder '$expandFolder'" - } - - Write-Host "Extracting folder for '$platform' environment from VSIX to '$expandFolder'" - try { - Expand-Archive -Path $newPath -DestinationPath $expandFolder -Force - } catch { - Write-Error "Expand-Archive failed: $($_.Exception.Message)" - exit 1 - } - - # Step 5: Finish - $expectedEnvPath = if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - "win32" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - "win32" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - "linux" - } - - $expectedCompilerName = if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - "alc.exe" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - "alc.exe" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - "alc" - } - - $ALEXEPath = Join-Path -Path $expandFolder -ChildPath (Join-Path -Path "extension" -ChildPath (Join-Path -Path "bin" -ChildPath $expectedEnvPath)) - - $ActualALEXE = Join-Path -Path $ALEXEPath -ChildPath $expectedCompilerName - - #Debug section - Write-Debug "########################################################################################################################################" - Write-Debug "Enumerating filesystem from '$expandFolder'" - Write-Debug "File size: $((Get-Item $newPath).Length)" - Write-Debug "########################################################################################################################################" - Write-Debug "" - Get-ChildItem -Path "$expandFolder" -Force -Recurse | ForEach-Object { Write-Debug $_.FullName } - Write-Debug "" - Write-Debug "########################################################################################################################################" - - #/Debug section - Write-Host "Testing destination: $ActualALEXE" - if (Test-Path -Path $ActualALEXE) { - Write-Host "Routine complete; ALC[.EXE] should be located at $ALEXEPath, called '$expectedCompilerName'" - Write-Host "Returning ALEXEPath from to function call" - - return [PSCustomObject]@{ - ALEXEPath = $ALEXEPath - Version = $getVersion - } - } else { - Write-Error "'$ActualALEXE' did not resolve to a correct location. Enumerating file system for reference:" - Write-Host "" - Get-ChildItem -Path "$(Build.SourcesDirectory)" -Force -Recurse | ForEach-Object { Write-Host $_.FullName } - } -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/FindFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/FindFunctions.ps1 deleted file mode 100644 index c0278ea..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/FindFunctions.ps1 +++ /dev/null @@ -1,728 +0,0 @@ -<# -.SYNOPSIS -Finds files using match patterns. - -.DESCRIPTION -Determines the find root from a list of patterns. Performs the find and then applies the glob patterns. Supports interleaved exclude patterns. Unrooted patterns are rooted using defaultRoot, unless matchOptions.matchBase is specified and the pattern is a basename only. For matchBase cases, the defaultRoot is used as the find root. - -.PARAMETER DefaultRoot -Default path to root unrooted patterns. Falls back to System.DefaultWorkingDirectory or current location. - -.PARAMETER Pattern -Patterns to apply. Supports interleaved exclude patterns. - -.PARAMETER FindOptions -When the FindOptions parameter is not specified, defaults to (New-VstsFindOptions -FollowSymbolicLinksTrue). Following soft links is generally appropriate unless deleting files. - -.PARAMETER MatchOptions -When the MatchOptions parameter is not specified, defaults to (New-VstsMatchOptions -Dot -NoBrace -NoCase). -#> -function Find-Match { - [CmdletBinding()] - param( - [Parameter()] - [string]$DefaultRoot, - [Parameter()] - [string[]]$Pattern, - $FindOptions, - $MatchOptions) - - Trace-EnteringInvocation $MyInvocation -Parameter None - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Apply defaults for parameters and trace. - if (!$DefaultRoot) { - $DefaultRoot = Get-TaskVariable -Name 'System.DefaultWorkingDirectory' -Default (Get-Location).Path - } - - Write-Verbose "DefaultRoot: '$DefaultRoot'" - if (!$FindOptions) { - $FindOptions = New-FindOptions -FollowSpecifiedSymbolicLink -FollowSymbolicLinks - } - - Trace-FindOptions -Options $FindOptions - if (!$MatchOptions) { - $MatchOptions = New-MatchOptions -Dot -NoBrace -NoCase - } - - Trace-MatchOptions -Options $MatchOptions - Add-Type -LiteralPath $PSScriptRoot\Minimatch.dll - - # Normalize slashes for root dir. - $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot - - $results = @{ } - $originalMatchOptions = $MatchOptions - foreach ($pat in $Pattern) { - Write-Verbose "Pattern: '$pat'" - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue - } - - # Clone match options. - $MatchOptions = Copy-MatchOptions -Options $originalMatchOptions - - # Skip comments. - if (!$MatchOptions.NoComment -and $pat.StartsWith('#')) { - Write-Verbose 'Skipping comment.' - continue - } - - # Set NoComment. Brace expansion could result in a leading '#'. - $MatchOptions.NoComment = $true - - # Determine whether pattern is include or exclude. - $negateCount = 0 - if (!$MatchOptions.NoNegate) { - while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') { - $negateCount++ - } - - $pat = $pat.Substring($negateCount) # trim leading '!' - if ($negateCount) { - Write-Verbose "Trimmed leading '!'. Pattern: '$pat'" - } - } - - $isIncludePattern = $negateCount -eq 0 -or - ($negateCount % 2 -eq 0 -and !$MatchOptions.FlipNegate) -or - ($negateCount % 2 -eq 1 -and $MatchOptions.FlipNegate) - - # Set NoNegate. Brace expansion could result in a leading '!'. - $MatchOptions.NoNegate = $true - $MatchOptions.FlipNegate = $false - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue - } - - # Expand braces - required to accurately interpret findPath. - $expanded = $null - $preExpanded = $pat - if ($MatchOptions.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 $MatchOptions)) - } - - # Set NoBrace. - $MatchOptions.NoBrace = $true - - foreach ($pat in $expanded) { - if ($pat -ne $preExpanded) { - Write-Verbose "Pattern: '$pat'" - } - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose "Skipping empty pattern." - continue - } - - if ($isIncludePattern) { - # Determine the findPath. - $findInfo = Get-FindInfoFromPattern -DefaultRoot $DefaultRoot -Pattern $pat -MatchOptions $MatchOptions - $findPath = $findInfo.FindPath - Write-Verbose "FindPath: '$findPath'" - - if (!$findPath) { - Write-Verbose "Skipping empty path." - continue - } - - # Perform the find. - Write-Verbose "StatOnly: '$($findInfo.StatOnly)'" - [string[]]$findResults = @( ) - if ($findInfo.StatOnly) { - # Simply stat the path - all path segments were used to build the path. - if ((Test-Path -LiteralPath $findPath)) { - $findResults += $findPath - } - } else { - $findResults = @( Get-FindResult -Path $findPath -Options $FindOptions ) - } - - Write-Verbose "Found $($findResults.Count) paths." - - # Apply the pattern. - Write-Verbose "Applying include pattern." - if ($findInfo.AdjustedPattern -ne $pat) { - Write-Verbose "AdjustedPattern: '$($findInfo.AdjustedPattern)'" - $pat = $findInfo.AdjustedPattern - } - - $matchResults = [Minimatch.Minimatcher]::Filter( - $findResults, - $pat, - (ConvertTo-MinimatchOptions -Options $MatchOptions)) - - # Union the results. - $matchCount = 0 - foreach ($matchResult in $matchResults) { - $matchCount++ - $results[$matchResult.ToUpperInvariant()] = $matchResult - } - - Write-Verbose "$matchCount matches" - } else { - # Check if basename only and MatchBase=true. - if ($MatchOptions.MatchBase -and - !(Test-Rooted -Path $pat) -and - ($pat -replace '\\', '/').IndexOf('/') -lt 0) { - - # Do not root the pattern. - Write-Verbose "MatchBase and basename only." - } else { - # Root the exclude pattern. - $pat = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $pat - Write-Verbose "After Get-RootedPattern, pattern: '$pat'" - } - - # Apply the pattern. - Write-Verbose 'Applying exclude pattern.' - $matchResults = [Minimatch.Minimatcher]::Filter( - [string[]]$results.Values, - $pat, - (ConvertTo-MinimatchOptions -Options $MatchOptions)) - - # Subtract the results. - $matchCount = 0 - foreach ($matchResult in $matchResults) { - $matchCount++ - $results.Remove($matchResult.ToUpperInvariant()) - } - - Write-Verbose "$matchCount matches" - } - } - } - - $finalResult = @( $results.Values | Sort-Object ) - Write-Verbose "$($finalResult.Count) final results" - return $finalResult - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Creates FindOptions for use with Find-VstsMatch. - -.DESCRIPTION -Creates FindOptions for use with Find-VstsMatch. Contains switches to control whether to follow symlinks. - -.PARAMETER FollowSpecifiedSymbolicLink -Indicates whether to traverse descendants if the specified path is a symbolic link directory. Does not cause nested symbolic link directories to be traversed. - -.PARAMETER FollowSymbolicLinks -Indicates whether to traverse descendants of symbolic link directories. -#> -function New-FindOptions { - [CmdletBinding()] - param( - [switch]$FollowSpecifiedSymbolicLink, - [switch]$FollowSymbolicLinks) - - return New-Object psobject -Property @{ - FollowSpecifiedSymbolicLink = $FollowSpecifiedSymbolicLink.IsPresent - FollowSymbolicLinks = $FollowSymbolicLinks.IsPresent - } -} - -<# -.SYNOPSIS -Creates MatchOptions for use with Find-VstsMatch and Select-VstsMatch. - -.DESCRIPTION -Creates MatchOptions for use with Find-VstsMatch and Select-VstsMatch. Contains switches to control which pattern matching options are applied. -#> -function New-MatchOptions { - [CmdletBinding()] - param( - [switch]$Dot, - [switch]$FlipNegate, - [switch]$MatchBase, - [switch]$NoBrace, - [switch]$NoCase, - [switch]$NoComment, - [switch]$NoExt, - [switch]$NoGlobStar, - [switch]$NoNegate, - [switch]$NoNull) - - return New-Object psobject -Property @{ - Dot = $Dot.IsPresent - FlipNegate = $FlipNegate.IsPresent - MatchBase = $MatchBase.IsPresent - NoBrace = $NoBrace.IsPresent - NoCase = $NoCase.IsPresent - NoComment = $NoComment.IsPresent - NoExt = $NoExt.IsPresent - NoGlobStar = $NoGlobStar.IsPresent - NoNegate = $NoNegate.IsPresent - NoNull = $NoNull.IsPresent - } -} - -<# -.SYNOPSIS -Applies match patterns against a list of files. - -.DESCRIPTION -Applies match patterns to a list of paths. Supports interleaved exclude patterns. - -.PARAMETER ItemPath -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). -#> -function Select-Match { - [CmdletBinding()] - param( - [Parameter()] - [string[]]$ItemPath, - [Parameter()] - [string[]]$Pattern, - [Parameter()] - [string]$PatternRoot, - $Options) - - - Trace-EnteringInvocation $MyInvocation -Parameter None - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - if (!$Options) { - $Options = New-MatchOptions -Dot -NoBrace -NoCase - } - - Trace-MatchOptions -Options $Options - Add-Type -LiteralPath $PSScriptRoot\Minimatch.dll - - # Hashtable to keep track of matches. - $map = @{ } - - $originalOptions = $Options - foreach ($pat in $Pattern) { - Write-Verbose "Pattern: '$pat'" - - # Trim and skip empty. - $pat = "$pat".Trim() - if (!$pat) { - Write-Verbose 'Skipping empty pattern.' - continue - } - - # Clone match options. - $Options = Copy-MatchOptions -Options $originalOptions - - # Skip comments. - if (!$Options.NoComment -and $pat.StartsWith('#')) { - Write-Verbose 'Skipping comment.' - continue - } - - # Set NoComment. Brace expansion could result in a leading '#'. - $Options.NoComment = $true - - # Determine whether pattern is include or exclude. - $negateCount = 0 - if (!$Options.NoNegate) { - while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') { - $negateCount++ - } - - $pat = $pat.Substring($negateCount) # trim leading '!' - if ($negateCount) { - Write-Verbose "Trimmed leading '!'. Pattern: '$pat'" - } - } - - $isIncludePattern = $negateCount -eq 0 -or - ($negateCount % 2 -eq 0 -and !$Options.FlipNegate) -or - ($negateCount % 2 -eq 1 -and $Options.FlipNegate) - - # Set NoNegate. Brace expansion could result in a leading '!'. - $Options.NoNegate = $true - $Options.FlipNegate = $false - - # 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)) - } - - # Set NoBrace. - $Options.NoBrace = $true - - foreach ($pat in $expanded) { - if ($pat -ne $preExpanded) { - Write-Verbose "Pattern: '$pat'" - } - - # 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)) { - - # Root the include pattern. - $pat = Get-RootedPattern -DefaultRoot $PatternRoot -Pattern $pat - Write-Verbose "After Get-RootedPattern, pattern: '$pat'" - } - - 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" - } - } - } - - # return a filtered version of the original list (preserves order and prevents duplication) - $result = $ItemPath | Where-Object { $map[$_] } - Write-Verbose "$($result.Count) final results" - $result - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -################################################################################ -# Private functions. -################################################################################ - -function Copy-MatchOptions { - [CmdletBinding()] - param($Options) - - return New-Object psobject -Property @{ - Dot = $Options.Dot -eq $true - FlipNegate = $Options.FlipNegate -eq $true - MatchBase = $Options.MatchBase -eq $true - NoBrace = $Options.NoBrace -eq $true - NoCase = $Options.NoCase -eq $true - NoComment = $Options.NoComment -eq $true - NoExt = $Options.NoExt -eq $true - NoGlobStar = $Options.NoGlobStar -eq $true - NoNegate = $Options.NoNegate -eq $true - NoNull = $Options.NoNull -eq $true - } -} - -function ConvertTo-MinimatchOptions { - [CmdletBinding()] - param($Options) - - $opt = New-Object Minimatch.Options - $opt.AllowWindowsPaths = $true - $opt.Dot = $Options.Dot -eq $true - $opt.FlipNegate = $Options.FlipNegate -eq $true - $opt.MatchBase = $Options.MatchBase -eq $true - $opt.NoBrace = $Options.NoBrace -eq $true - $opt.NoCase = $Options.NoCase -eq $true - $opt.NoComment = $Options.NoComment -eq $true - $opt.NoExt = $Options.NoExt -eq $true - $opt.NoGlobStar = $Options.NoGlobStar -eq $true - $opt.NoNegate = $Options.NoNegate -eq $true - $opt.NoNull = $Options.NoNull -eq $true - return $opt -} - -function ConvertTo-NormalizedSeparators { - [CmdletBinding()] - param([string]$Path) - - # Convert slashes. - $Path = "$Path".Replace('/', '\') - - # Remove redundant slashes. - $isUnc = $Path -match '^\\\\+[^\\]' - $Path = $Path -replace '\\\\+', '\' - if ($isUnc) { - $Path = '\' + $Path - } - - return $Path -} - -function Get-FindInfoFromPattern { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$DefaultRoot, - [Parameter(Mandatory = $true)] - [string]$Pattern, - [Parameter(Mandatory = $true)] - $MatchOptions) - - if (!$MatchOptions.NoBrace) { - throw "Get-FindInfoFromPattern expected MatchOptions.NoBrace to be true." - } - - # For the sake of determining the find path, pretend NoCase=false. - $MatchOptions = Copy-MatchOptions -Options $MatchOptions - $MatchOptions.NoCase = $false - - # Check if basename only and MatchBase=true - if ($MatchOptions.MatchBase -and - !(Test-Rooted -Path $Pattern) -and - ($Pattern -replace '\\', '/').IndexOf('/') -lt 0) { - - return New-Object psobject -Property @{ - AdjustedPattern = $Pattern - FindPath = $DefaultRoot - StatOnly = $false - } - } - - # The technique applied by this function is to use the information on the Minimatch object determine - # the findPath. Minimatch breaks the pattern into path segments, and exposes information about which - # segments are literal vs patterns. - # - # Note, the technique currently imposes a limitation for drive-relative paths with a glob in the - # first segment, e.g. C:hello*/world. It's feasible to overcome this limitation, but is left unsolved - # for now. - $minimatchObj = New-Object Minimatch.Minimatcher($Pattern, (ConvertTo-MinimatchOptions -Options $MatchOptions)) - - # The "set" field is a two-dimensional enumerable of parsed path segment info. The outer enumerable should only - # contain one item, otherwise something went wrong. Brace expansion can result in multiple items in the outer - # enumerable, but that should be turned off by the time this function is reached. - # - # Note, "set" is a private field in the .NET implementation but is documented as a feature in the nodejs - # implementation. The .NET implementation is a port and is by a different author. - $setFieldInfo = $minimatchObj.GetType().GetField('set', 'Instance,NonPublic') - [object[]]$set = $setFieldInfo.GetValue($minimatchObj) - if ($set.Count -ne 1) { - throw "Get-FindInfoFromPattern expected Minimatch.Minimatcher(...).set.Count to be 1. Actual: '$($set.Count)'" - } - - [string[]]$literalSegments = @( ) - [object[]]$parsedSegments = $set[0] - foreach ($parsedSegment in $parsedSegments) { - if ($parsedSegment.GetType().Name -eq 'LiteralItem') { - # The item is a LiteralItem when the original input for the path segment does not contain any - # unescaped glob characters. - $literalSegments += $parsedSegment.Source; - continue - } - - break; - } - - # Join the literal segments back together. Minimatch converts '\' to '/' on Windows, then squashes - # consequetive slashes, and finally splits on slash. This means that UNC format is lost, but can - # be detected from the original pattern. - $joinedSegments = [string]::Join('/', $literalSegments) - if ($joinedSegments -and ($Pattern -replace '\\', '/').StartsWith('//')) { - $joinedSegments = '/' + $joinedSegments # restore UNC format - } - - # Determine the find path. - $findPath = '' - if ((Test-Rooted -Path $Pattern)) { # The pattern is rooted. - $findPath = $joinedSegments - } elseif ($joinedSegments) { # The pattern is not rooted, and literal segements were found. - $findPath = [System.IO.Path]::Combine($DefaultRoot, $joinedSegments) - } else { # The pattern is not rooted, and no literal segements were found. - $findPath = $DefaultRoot - } - - # Clean up the path. - if ($findPath) { - $findPath = [System.IO.Path]::GetDirectoryName(([System.IO.Path]::Combine($findPath, '_'))) # Hack to remove unnecessary trailing slash. - $findPath = ConvertTo-NormalizedSeparators -Path $findPath - } - - return New-Object psobject -Property @{ - AdjustedPattern = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $Pattern - FindPath = $findPath - StatOnly = $literalSegments.Count -eq $parsedSegments.Count - } -} - -function Get-FindResult { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - $Options) - - if (!(Test-Path -LiteralPath $Path)) { - Write-Verbose 'Path not found.' - return - } - - $Path = ConvertTo-NormalizedSeparators -Path $Path - - # Push the first item. - [System.Collections.Stack]$stack = New-Object System.Collections.Stack - $stack.Push((Get-Item -LiteralPath $Path)) - - $count = 0 - while ($stack.Count) { - # Pop the next item and yield the result. - $item = $stack.Pop() - $count++ - $item.FullName - - # Traverse. - if (($item.Attributes -band 0x00000010) -eq 0x00000010) { # Directory - if (($item.Attributes -band 0x00000400) -ne 0x00000400 -or # ReparsePoint - $Options.FollowSymbolicLinks -or - ($count -eq 1 -and $Options.FollowSpecifiedSymbolicLink)) { - - $childItems = @( Get-DirectoryChildItem -Path $Item.FullName -Force ) - [System.Array]::Reverse($childItems) - foreach ($childItem in $childItems) { - $stack.Push($childItem) - } - } - } - } -} - -function Get-RootedPattern { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$DefaultRoot, - [Parameter(Mandatory = $true)] - [string]$Pattern) - - if ((Test-Rooted -Path $Pattern)) { - return $Pattern - } - - # Normalize root. - $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot - - # Escape special glob characters. - $DefaultRoot = $DefaultRoot -replace '(\[)(?=[^\/]+\])', '[[]' # Escape '[' when ']' follows within the path segment - $DefaultRoot = $DefaultRoot.Replace('?', '[?]') # Escape '?' - $DefaultRoot = $DefaultRoot.Replace('*', '[*]') # Escape '*' - $DefaultRoot = $DefaultRoot -replace '\+\(', '[+](' # Escape '+(' - $DefaultRoot = $DefaultRoot -replace '@\(', '[@](' # Escape '@(' - $DefaultRoot = $DefaultRoot -replace '!\(', '[!](' # Escape '!(' - - if ($DefaultRoot -like '[A-Z]:') { # e.g. C: - return "$DefaultRoot$Pattern" - } - - # Ensure root ends with a separator. - if (!$DefaultRoot.EndsWith('\')) { - $DefaultRoot = "$DefaultRoot\" - } - - return "$DefaultRoot$Pattern" -} - -function Test-Rooted { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path) - - $Path = ConvertTo-NormalizedSeparators -Path $Path - return $Path.StartsWith('\') -or # e.g. \ or \hello or \\hello - $Path -like '[A-Z]:*' # e.g. C: or C:\hello -} - -function Trace-MatchOptions { - [CmdletBinding()] - param($Options) - - Write-Verbose "MatchOptions.Dot: '$($Options.Dot)'" - Write-Verbose "MatchOptions.FlipNegate: '$($Options.FlipNegate)'" - Write-Verbose "MatchOptions.MatchBase: '$($Options.MatchBase)'" - Write-Verbose "MatchOptions.NoBrace: '$($Options.NoBrace)'" - Write-Verbose "MatchOptions.NoCase: '$($Options.NoCase)'" - Write-Verbose "MatchOptions.NoComment: '$($Options.NoComment)'" - Write-Verbose "MatchOptions.NoExt: '$($Options.NoExt)'" - Write-Verbose "MatchOptions.NoGlobStar: '$($Options.NoGlobStar)'" - Write-Verbose "MatchOptions.NoNegate: '$($Options.NoNegate)'" - Write-Verbose "MatchOptions.NoNull: '$($Options.NoNull)'" -} - -function Trace-FindOptions { - [CmdletBinding()] - param($Options) - - Write-Verbose "FindOptions.FollowSpecifiedSymbolicLink: '$($FindOptions.FollowSpecifiedSymbolicLink)'" - Write-Verbose "FindOptions.FollowSymbolicLinks: '$($FindOptions.FollowSymbolicLinks)'" -} diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/InputFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/InputFunctions.ps1 deleted file mode 100644 index 071d5ca..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/InputFunctions.ps1 +++ /dev/null @@ -1,524 +0,0 @@ -# Hash table of known variable info. The formatted env var name is the lookup key. -# -# The purpose of this hash table is to keep track of known variables. The hash table -# needs to be maintained for multiple reasons: -# 1) to distinguish between env vars and job vars -# 2) to distinguish between secret vars and public -# 3) to know the real variable name and not just the formatted env var name. -$script:knownVariables = @{ } -$script:vault = @{ } - -<# -.SYNOPSIS -Gets an endpoint. - -.DESCRIPTION -Gets an endpoint object for the specified endpoint name. The endpoint is returned as an object with three properties: Auth, Data, and Url. - -The Data property requires a 1.97 agent or higher. - -.PARAMETER Require -Writes an error to the error pipeline if the endpoint is not found. -#> -function Get-Endpoint { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [switch]$Require) - - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Get the URL. - $description = Get-LocString -Key PSLIB_EndpointUrl0 -ArgumentList $Name - $key = "ENDPOINT_URL_$Name" - $url = Get-VaultValue -Description $description -Key $key -Require:$Require - - # Get the auth object. - $description = Get-LocString -Key PSLIB_EndpointAuth0 -ArgumentList $Name - $key = "ENDPOINT_AUTH_$Name" - if ($auth = (Get-VaultValue -Description $description -Key $key -Require:$Require)) { - $auth = ConvertFrom-Json -InputObject $auth - } - - # Get the data. - $description = "'$Name' service endpoint data" - $key = "ENDPOINT_DATA_$Name" - if ($data = (Get-VaultValue -Description $description -Key $key)) { - $data = ConvertFrom-Json -InputObject $data - } - - # Return the endpoint. - if ($url -or $auth -or $data) { - New-Object -TypeName psobject -Property @{ - Url = $url - Auth = $auth - Data = $data - } - } - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } -} - -<# -.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. - -.DESCRIPTION -Gets the value for the specified input name. - -.PARAMETER AsBool -Returns the value as a bool. Returns true if the value converted to a string is "1" or "true" (case insensitive); otherwise false. - -.PARAMETER AsInt -Returns the value as an int. Returns the value converted to an int. Returns 0 if the conversion fails. - -.PARAMETER Default -Default value to use if the input is null or empty. - -.PARAMETER Require -Writes an error to the error pipeline if the input is null or empty. -#> -function Get-Input { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(ParameterSetName = 'Default')] - $Default, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [switch]$AsBool, - [switch]$AsInt) - - # Get the input from the vault. Splat the bound parameters hashtable. Splatting is required - # in order to concisely invoke the correct parameter set. - $null = $PSBoundParameters.Remove('Name') - $description = Get-LocString -Key PSLIB_Input0 -ArgumentList $Name - $key = "INPUT_$($Name.Replace(' ', '_').ToUpperInvariant())" - Get-VaultValue @PSBoundParameters -Description $description -Key $key -} - -<# -.SYNOPSIS -Gets a task variable. - -.DESCRIPTION -Gets the value for the specified task variable. - -.PARAMETER AsBool -Returns the value as a bool. Returns true if the value converted to a string is "1" or "true" (case insensitive); otherwise false. - -.PARAMETER AsInt -Returns the value as an int. Returns the value converted to an int. Returns 0 if the conversion fails. - -.PARAMETER Default -Default value to use if the input is null or empty. - -.PARAMETER Require -Writes an error to the error pipeline if the input is null or empty. -#> -function Get-TaskVariable { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(ParameterSetName = 'Default')] - $Default, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [switch]$AsBool, - [switch]$AsInt) - - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - $description = Get-LocString -Key PSLIB_TaskVariable0 -ArgumentList $Name - $variableKey = Get-VariableKey -Name $Name - if ($script:knownVariables.$variableKey.Secret) { - # Get secret variable. Splatting is required to concisely invoke the correct parameter set. - $null = $PSBoundParameters.Remove('Name') - $vaultKey = "SECRET_$variableKey" - Get-VaultValue @PSBoundParameters -Description $description -Key $vaultKey - } else { - # Get public variable. - $item = $null - $path = "Env:$variableKey" - if ((Test-Path -LiteralPath $path) -and ($item = Get-Item -LiteralPath $path).Value) { - # Intentionally empty. Value was successfully retrieved. - } elseif (!$script:nonInteractive) { - # The value wasn't found and the module is running in interactive dev mode. - # Prompt for the value. - Set-Item -LiteralPath $path -Value (Read-Host -Prompt $description) - if (Test-Path -LiteralPath $path) { - $item = Get-Item -LiteralPath $path - } - } - - # Get the converted value. Splatting is required to concisely invoke the correct parameter set. - $null = $PSBoundParameters.Remove('Name') - Get-Value @PSBoundParameters -Description $description -Key $variableKey -Value $item.Value - } - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } -} - -<# -.SYNOPSIS -Gets all job variables available to the task. Requires 2.104.1 agent or higher. - -.DESCRIPTION -Gets a snapshot of the current state of all job variables available to the task. -Requires a 2.104.1 agent or higher for full functionality. - -Returns an array of objects with the following properties: - [string]Name - [string]Value - [bool]Secret - -Limitations on an agent prior to 2.104.1: - 1) The return value does not include all public variables. Only public variables - that have been added using setVariable are returned. - 2) The name returned for each secret variable is the formatted environment variable - name, not the actual variable name (unless it was set explicitly at runtime using - setVariable). -#> -function Get-TaskVariableInfo { - [CmdletBinding()] - param() - - foreach ($info in $script:knownVariables.Values) { - New-Object -TypeName psobject -Property @{ - Name = $info.Name - Value = Get-TaskVariable -Name $info.Name - Secret = $info.Secret - } - } -} - -<# -.SYNOPSIS -Sets a task variable. - -.DESCRIPTION -Sets a task variable in the current task context as well as in the current job context. This allows the task variable to retrieved by subsequent tasks within the same job. -#> -function Set-TaskVariable { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [string]$Value, - [switch]$Secret) - - # Once a secret always a secret. - $variableKey = Get-VariableKey -Name $Name - [bool]$Secret = $Secret -or $script:knownVariables.$variableKey.Secret - if ($Secret) { - $vaultKey = "SECRET_$variableKey" - if (!$Value) { - # Clear the secret. - Write-Verbose "Set $Name = ''" - $script:vault.Remove($vaultKey) - } else { - # Store the secret in the vault. - Write-Verbose "Set $Name = '********'" - $script:vault[$vaultKey] = New-Object System.Management.Automation.PSCredential( - $vaultKey, - (ConvertTo-SecureString -String $Value -AsPlainText -Force)) - } - - # Clear the environment variable. - Set-Item -LiteralPath "Env:$variableKey" -Value '' - } else { - # Set the environment variable. - Write-Verbose "Set $Name = '$Value'" - Set-Item -LiteralPath "Env:$variableKey" -Value $Value - } - - # Store the metadata. - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - Name = $name - Secret = $Secret - } - - # Persist the variable in the task context. - Write-SetVariable -Name $Name -Value $Value -Secret:$Secret -} - -<# -.SYNOPSIS -Gets the value of an task feature and converts to a bool. - -.PARAMETER $FeatureName -Name of the feature to get. - -.NOTES -This method is only for internal Microsoft development. Do not use it for external tasks. -#> -function Get-PipelineFeature { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$FeatureName - ) - - $featureValue = Get-TaskVariable -Name "DistributedTask.Tasks.$FeatureName" - - if (!$featureValue) { - Write-Debug "Feature '$FeatureName' is not set. Defaulting to 'false'" - return $false - } - - $boolValue = $featureValue.ToLowerInvariant() -eq 'true' - - Write-Debug "Feature '$FeatureName' = '$featureValue'. Processed as '$boolValue'" - - return $boolValue -} - -######################################## -# Private functions. -######################################## -function Get-VaultValue { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [Parameter(Mandatory = $true)] - [string]$Description, - [Parameter(Mandatory = $true)] - [string]$Key, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [Parameter(ParameterSetName = 'Default')] - [object]$Default, - [switch]$AsBool, - [switch]$AsInt) - - # Attempt to get the vault value. - $value = $null - if ($psCredential = $script:vault[$Key]) { - $value = $psCredential.GetNetworkCredential().Password - } elseif (!$script:nonInteractive) { - # The value wasn't found. Prompt for the value if running in interactive dev mode. - $value = Read-Host -Prompt $Description - if ($value) { - $script:vault[$Key] = New-Object System.Management.Automation.PSCredential( - $Key, - (ConvertTo-SecureString -String $value -AsPlainText -Force)) - } - } - - Get-Value -Value $value @PSBoundParameters -} - -function Get-Value { - [CmdletBinding(DefaultParameterSetName = 'Require')] - param( - [string]$Value, - [Parameter(Mandatory = $true)] - [string]$Description, - [Parameter(Mandatory = $true)] - [string]$Key, - [Parameter(ParameterSetName = 'Require')] - [switch]$Require, - [Parameter(ParameterSetName = 'Default')] - [object]$Default, - [switch]$AsBool, - [switch]$AsInt) - - $result = $Value - if ($result) { - if ($Key -like 'ENDPOINT_AUTH_*') { - Write-Verbose "$($Key): '********'" - } else { - Write-Verbose "$($Key): '$result'" - } - } else { - Write-Verbose "$Key (empty)" - - # Write error if required. - if ($Require) { - Write-Error "$(Get-LocString -Key PSLIB_Required0 $Description)" - return - } - - # Fallback to the default if provided. - if ($PSCmdlet.ParameterSetName -eq 'Default') { - $result = $Default - $OFS = ' ' - Write-Verbose " Defaulted to: '$result'" - } else { - $result = '' - } - } - - # Convert to bool if specified. - if ($AsBool) { - if ($result -isnot [bool]) { - $result = "$result" -in '1', 'true' - Write-Verbose " Converted to bool: $result" - } - - return $result - } - - # Convert to int if specified. - if ($AsInt) { - if ($result -isnot [int]) { - try { - $result = [int]"$result" - } catch { - $result = 0 - } - - Write-Verbose " Converted to int: $result" - } - - return $result - } - - return $result -} - -function Initialize-Inputs { - # Store endpoints, inputs, and secret variables in the vault. - foreach ($variable in (Get-ChildItem -Path Env:ENDPOINT_?*, Env:INPUT_?*, Env:SECRET_?*, Env:SECUREFILE_?*)) { - # Record the secret variable metadata. This is required by Get-TaskVariable to - # retrieve the value. In a 2.104.1 agent or higher, this metadata will be overwritten - # when $env:VSTS_SECRET_VARIABLES is processed. - if ($variable.Name -like 'SECRET_?*') { - $variableKey = $variable.Name.Substring('SECRET_'.Length) - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - # This is technically not the variable name (has underscores instead of dots), - # but it's good enough to make Get-TaskVariable work in a pre-2.104.1 agent - # where $env:VSTS_SECRET_VARIABLES is not defined. - Name = $variableKey - Secret = $true - } - } - - # Store the value in the vault. - $vaultKey = $variable.Name - if ($variable.Value) { - $script:vault[$vaultKey] = New-Object System.Management.Automation.PSCredential( - $vaultKey, - (ConvertTo-SecureString -String $variable.Value -AsPlainText -Force)) - } - - # Clear the environment variable. - Remove-Item -LiteralPath "Env:$($variable.Name)" - } - - # Record the public variable names. Env var added in 2.104.1 agent. - if ($env:VSTS_PUBLIC_VARIABLES) { - foreach ($name in (ConvertFrom-Json -InputObject $env:VSTS_PUBLIC_VARIABLES)) { - $variableKey = Get-VariableKey -Name $name - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - Name = $name - Secret = $false - } - } - - $env:VSTS_PUBLIC_VARIABLES = '' - } - - # Record the secret variable names. Env var added in 2.104.1 agent. - if ($env:VSTS_SECRET_VARIABLES) { - foreach ($name in (ConvertFrom-Json -InputObject $env:VSTS_SECRET_VARIABLES)) { - $variableKey = Get-VariableKey -Name $name - $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ - Name = $name - Secret = $true - } - } - - $env:VSTS_SECRET_VARIABLES = '' - } -} - -function Get-VariableKey { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name) - - if ($Name -ne 'agent.jobstatus') { - $Name = $Name.Replace('.', '_') - } - - $Name.ToUpperInvariant() -} diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 deleted file mode 100644 index 9e9e9ec..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1 +++ /dev/null @@ -1,320 +0,0 @@ -<# -.SYNOPSIS -Finds files or directories. - -.DESCRIPTION -Finds files or directories using advanced pattern matching. - -.PARAMETER LiteralDirectory -Directory to search. - -.PARAMETER LegacyPattern -Proprietary pattern format. The LiteralDirectory parameter is used to root any unrooted patterns. - -Separate multiple patterns using ";". Escape actual ";" in the path by using ";;". -"?" indicates a wildcard that represents any single character within a path segment. -"*" indicates a wildcard that represents zero or more characters within a path segment. -"**" as the entire path segment indicates a recursive search. -"**" within a path segment indicates a recursive intersegment wildcard. -"+:" (can be omitted) indicates an include pattern. -"-:" indicates an exclude pattern. - -The result is from the command is a union of all the matches from the include patterns, minus the matches from the exclude patterns. - -.PARAMETER IncludeFiles -Indicates whether to include files in the results. - -If neither IncludeFiles or IncludeDirectories is set, then IncludeFiles is assumed. - -.PARAMETER IncludeDirectories -Indicates whether to include directories in the results. - -If neither IncludeFiles or IncludeDirectories is set, then IncludeFiles is assumed. - -.PARAMETER Force -Indicates whether to include hidden items. - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\Is?Match.txt" - -Given: -C:\Directory\Is1Match.txt -C:\Directory\Is2Match.txt -C:\Directory\IsNotMatch.txt - -Returns: -C:\Directory\Is1Match.txt -C:\Directory\Is2Match.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\Is*Match.txt" - -Given: -C:\Directory\IsOneMatch.txt -C:\Directory\IsTwoMatch.txt -C:\Directory\NonMatch.txt - -Returns: -C:\Directory\IsOneMatch.txt -C:\Directory\IsTwoMatch.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\**\Match.txt" - -Given: -C:\Directory\Match.txt -C:\Directory\NotAMatch.txt -C:\Directory\SubDir\Match.txt -C:\Directory\SubDir\SubSubDir\Match.txt - -Returns: -C:\Directory\Match.txt -C:\Directory\SubDir\Match.txt -C:\Directory\SubDir\SubSubDir\Match.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\**" - -Given: -C:\Directory\One.txt -C:\Directory\SubDir\Two.txt -C:\Directory\SubDir\SubSubDir\Three.txt - -Returns: -C:\Directory\One.txt -C:\Directory\SubDir\Two.txt -C:\Directory\SubDir\SubSubDir\Three.txt - -.EXAMPLE -Find-VstsFiles -LegacyPattern "C:\Directory\Sub**Match.txt" - -Given: -C:\Directory\IsNotAMatch.txt -C:\Directory\SubDir\IsAMatch.txt -C:\Directory\SubDir\IsNot.txt -C:\Directory\SubDir\SubSubDir\IsAMatch.txt -C:\Directory\SubDir\SubSubDir\IsNot.txt - -Returns: -C:\Directory\SubDir\IsAMatch.txt -C:\Directory\SubDir\SubSubDir\IsAMatch.txt -#> -function Find-Files { - [CmdletBinding()] - param( - [ValidateNotNullOrEmpty()] - [Parameter()] - [string]$LiteralDirectory, - [Parameter(Mandatory = $true)] - [string]$LegacyPattern, - [switch]$IncludeFiles, - [switch]$IncludeDirectories, - [switch]$Force) - - # Note, due to subtle implementation details of Get-PathPrefix/Get-PathIterator, - # this function does not appear to be able to search the root of a drive and other - # cases where Path.GetDirectoryName() returns empty. More details in Get-PathPrefix. - - Trace-EnteringInvocation $MyInvocation - if (!$IncludeFiles -and !$IncludeDirectories) { - $IncludeFiles = $true - } - - $includePatterns = New-Object System.Collections.Generic.List[string] - $excludePatterns = New-Object System.Collections.Generic.List[System.Text.RegularExpressions.Regex] - $LegacyPattern = $LegacyPattern.Replace(';;', "`0") - foreach ($pattern in $LegacyPattern.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)) { - $pattern = $pattern.Replace("`0", ';') - $isIncludePattern = Test-IsIncludePattern -Pattern ([ref]$pattern) - if ($LiteralDirectory -and !([System.IO.Path]::IsPathRooted($pattern))) { - # Use the root directory provided to make the pattern a rooted path. - $pattern = [System.IO.Path]::Combine($LiteralDirectory, $pattern) - } - - # Validate pattern does not end with a \. - if ($pattern.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { - throw (Get-LocString -Key PSLIB_InvalidPattern0 -ArgumentList $pattern) - } - - if ($isIncludePattern) { - $includePatterns.Add($pattern) - } else { - $excludePatterns.Add((Convert-PatternToRegex -Pattern $pattern)) - } - } - - $count = 0 - foreach ($path in (Get-MatchingItems -IncludePatterns $includePatterns -ExcludePatterns $excludePatterns -IncludeFiles:$IncludeFiles -IncludeDirectories:$IncludeDirectories -Force:$Force)) { - $count++ - $path - } - - Write-Verbose "Total found: $count" - Trace-LeavingInvocation $MyInvocation -} - -######################################## -# Private functions. -######################################## -function Convert-PatternToRegex { - [CmdletBinding()] - param([string]$Pattern) - - $Pattern = [regex]::Escape($Pattern.Replace('\', '/')). # Normalize separators and regex escape. - Replace('/\*\*/', '((/.+/)|(/))'). # Replace directory globstar. - Replace('\*\*', '.*'). # Replace remaining globstars with a wildcard that can span directory separators. - Replace('\*', '[^/]*'). # Replace asterisks with a wildcard that cannot span directory separators. - # bug: should be '[^/]' instead of '.' - Replace('\?', '.') # Replace single character wildcards. - New-Object regex -ArgumentList "^$Pattern`$", ([System.Text.RegularExpressions.RegexOptions]::IgnoreCase) -} - -function Get-FileNameFilter { - [CmdletBinding()] - param([string]$Pattern) - - $index = $Pattern.LastIndexOf('\') - if ($index -eq -1 -or # Pattern does not contain a backslash. - !($Pattern = $Pattern.Substring($index + 1)) -or # Pattern ends in a backslash. - $Pattern.Contains('**')) # Last segment contains an inter-segment wildcard. - { - return '*' - } - - # bug? is this supposed to do substring? - return $Pattern -} - -function Get-MatchingItems { - [CmdletBinding()] - param( - [System.Collections.Generic.List[string]]$IncludePatterns, - [System.Collections.Generic.List[regex]]$ExcludePatterns, - [switch]$IncludeFiles, - [switch]$IncludeDirectories, - [switch]$Force) - - Trace-EnteringInvocation $MyInvocation - $allFiles = New-Object System.Collections.Generic.HashSet[string] - foreach ($pattern in $IncludePatterns) { - $pathPrefix = Get-PathPrefix -Pattern $pattern - $fileNameFilter = Get-FileNameFilter -Pattern $pattern - $patternRegex = Convert-PatternToRegex -Pattern $pattern - # Iterate over the directories and files under the pathPrefix. - Get-PathIterator -Path $pathPrefix -Filter $fileNameFilter -IncludeFiles:$IncludeFiles -IncludeDirectories:$IncludeDirectories -Force:$Force | - ForEach-Object { - # Normalize separators. - $normalizedPath = $_.Replace('\', '/') - # **/times/** will not match C:/fun/times because there isn't a trailing slash. - # So try both if including directories. - $alternatePath = "$normalizedPath/" # potential bug: it looks like this will result in a false - # positive if the item is a regular file and not a directory - - $isMatch = $false - if ($patternRegex.IsMatch($normalizedPath) -or ($IncludeDirectories -and $patternRegex.IsMatch($alternatePath))) { - $isMatch = $true - - # Test whether the path should be excluded. - foreach ($regex in $ExcludePatterns) { - if ($regex.IsMatch($normalizedPath) -or ($IncludeDirectories -and $regex.IsMatch($alternatePath))) { - $isMatch = $false - break - } - } - } - - if ($isMatch) { - $null = $allFiles.Add($_) - } - } - } - - Trace-Path -Path $allFiles -PassThru - Trace-LeavingInvocation $MyInvocation -} - -function Get-PathIterator { - [CmdletBinding()] - param( - [string]$Path, - [string]$Filter, - [switch]$IncludeFiles, - [switch]$IncludeDirectories, - [switch]$Force) - - if (!$Path) { - return - } - - # bug: this returns the dir without verifying whether exists - if ($IncludeDirectories) { - $Path - } - - Get-DirectoryChildItem -Path $Path -Filter $Filter -Force:$Force -Recurse | - ForEach-Object { - if ($_.Attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { - if ($IncludeDirectories) { - $_.FullName - } - } elseif ($IncludeFiles) { - $_.FullName - } - } -} - -function Get-PathPrefix { - [CmdletBinding()] - param([string]$Pattern) - - # Note, unable to search root directories is a limitation due to subtleties of this function - # and downstream code in Get-PathIterator that short-circuits when the path prefix is empty. - # This function uses Path.GetDirectoryName() to determine the path prefix, which will yield - # empty in some cases. See the following examples of Path.GetDirectoryName() input => output: - # C:/ => - # C:/hello => C:\ - # C:/hello/ => C:\hello - # C:/hello/world => C:\hello - # C:/hello/world/ => C:\hello\world - # C: => - # C:hello => C: - # C:hello/ => C:hello - # / => - # /hello => \ - # /hello/ => \hello - # //hello => - # //hello/ => - # //hello/world => - # //hello/world/ => \\hello\world - - $index = $Pattern.IndexOfAny([char[]]@('*'[0], '?'[0])) - if ($index -eq -1) { - # If no wildcards are found, return the directory name portion of the path. - # If there is no directory name (file name only in pattern), this will return empty string. - return [System.IO.Path]::GetDirectoryName($Pattern) - } - - [System.IO.Path]::GetDirectoryName($Pattern.Substring(0, $index)) -} - -function Test-IsIncludePattern { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [ref]$Pattern) - - # Include patterns start with +: or anything except -: - # Exclude patterns start with -: - if ($Pattern.value.StartsWith("+:")) { - # Remove the prefix. - $Pattern.value = $Pattern.value.Substring(2) - $true - } elseif ($Pattern.value.StartsWith("-:")) { - # Remove the prefix. - $Pattern.value = $Pattern.value.Substring(2) - $false - } else { - # No prefix, so leave the string alone. - $true; - } -} diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 deleted file mode 100644 index b554970..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1 +++ /dev/null @@ -1,150 +0,0 @@ -$script:resourceStrings = @{ } - -<# -.SYNOPSIS -Gets a localized resource string. - -.DESCRIPTION -Gets a localized resource string and optionally formats the string with arguments. - -If the format fails (due to a bad format string or incorrect expected arguments in the format string), then the format string is returned followed by each of the arguments (delimited by a space). - -If the lookup key is not found, then the lookup key is returned followed by each of the arguments (delimited by a space). - -.PARAMETER Require -Writes an error to the error pipeline if the endpoint is not found. -#> -function Get-LocString { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true, Position = 1)] - [string]$Key, - [Parameter(Position = 2)] - [object[]]$ArgumentList = @( )) - - # Due to the dynamically typed nature of PowerShell, a single null argument passed - # to an array parameter is interpreted as a null array. - if ([object]::ReferenceEquals($null, $ArgumentList)) { - $ArgumentList = @( $null ) - } - - # Lookup the format string. - $format = '' - if (!($format = $script:resourceStrings[$Key])) { - # Warn the key was not found. Prevent recursion if the lookup key is the - # "string resource key not found" lookup key. - $resourceNotFoundKey = 'PSLIB_StringResourceKeyNotFound0' - if ($key -ne $resourceNotFoundKey) { - Write-Warning (Get-LocString -Key $resourceNotFoundKey -ArgumentList $Key) - } - - # Fallback to just the key itself if there aren't any arguments to format. - if (!$ArgumentList.Count) { return $key } - - # Otherwise fallback to the key followed by the arguments. - $OFS = " " - return "$key $ArgumentList" - } - - # Return the string if there aren't any arguments to format. - if (!$ArgumentList.Count) { return $format } - - try { - [string]::Format($format, $ArgumentList) - } catch { - Write-Warning (Get-LocString -Key 'PSLIB_StringFormatFailed') - $OFS = " " - "$format $ArgumentList" - } -} - -<# -.SYNOPSIS -Imports resource strings for use with GetVstsLocString. - -.DESCRIPTION -Imports resource strings for use with GetVstsLocString. The imported strings are stored in an internal resource string dictionary. Optionally, if a separate resource file for the current culture exists, then the localized strings from that file then imported (overlaid) into the same internal resource string dictionary. - -Resource strings from the SDK are prefixed with "PSLIB_". This prefix should be avoided for custom resource strings. - -.Parameter LiteralPath -JSON file containing resource strings. - -.EXAMPLE -Import-VstsLocStrings -LiteralPath $PSScriptRoot\Task.json - -Imports strings from messages section in the JSON file. If a messages section is not defined, then no strings are imported. Example messages section: -{ - "messages": { - "Hello": "Hello you!", - "Hello0": "Hello {0}!" - } -} - -.EXAMPLE -Import-VstsLocStrings -LiteralPath $PSScriptRoot\Task.json - -Overlays strings from an optional separate resource file for the current culture. - -Given the task variable System.Culture is set to 'de-DE'. This variable is set by the agent based on the current culture for the job. -Given the file Task.json contains: -{ - "messages": { - "GoodDay": "Good day!", - } -} -Given the file resources.resjson\de-DE\resources.resjson: -{ - "loc.messages.GoodDay": "Guten Tag!" -} - -The net result from the import command would be one new key-value pair added to the internal dictionary: Key = 'GoodDay', Value = 'Guten Tag!' -#> -function Import-LocStrings { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$LiteralPath) - - # Validate the file exists. - if (!(Test-Path -LiteralPath $LiteralPath -PathType Leaf)) { - Write-Warning (Get-LocString -Key PSLIB_FileNotFound0 -ArgumentList $LiteralPath) - return - } - - # Load the json. - Write-Verbose "Loading resource strings from: $LiteralPath" - $count = 0 - if ($messages = (Get-Content -LiteralPath $LiteralPath -Encoding UTF8 | Out-String | ConvertFrom-Json).messages) { - # Add each resource string to the hashtable. - foreach ($member in (Get-Member -InputObject $messages -MemberType NoteProperty)) { - [string]$key = $member.Name - $script:resourceStrings[$key] = $messages."$key" - $count++ - } - } - - Write-Verbose "Loaded $count strings." - - # Get the culture. - $culture = Get-TaskVariable -Name "System.Culture" -Default "en-US" - - # Load the resjson. - $resjsonPath = "$([System.IO.Path]::GetDirectoryName($LiteralPath))\Strings\resources.resjson\$culture\resources.resjson" - if (Test-Path -LiteralPath $resjsonPath) { - Write-Verbose "Loading resource strings from: $resjsonPath" - $count = 0 - $resjson = Get-Content -LiteralPath $resjsonPath -Encoding UTF8 | Out-String | ConvertFrom-Json - foreach ($member in (Get-Member -Name loc.messages.* -InputObject $resjson -MemberType NoteProperty)) { - if (!($value = $resjson."$($member.Name)")) { - continue - } - - [string]$key = $member.Name.Substring('loc.messages.'.Length) - $script:resourceStrings[$key] = $value - $count++ - } - - Write-Verbose "Loaded $count strings." - } -} diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 deleted file mode 100644 index 90400c7..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1 +++ /dev/null @@ -1,634 +0,0 @@ -$script:loggingCommandPrefix = '##vso[' -$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: Add test to verify don't need to escape "=". - -$commandCorrelationId = $env:COMMAND_CORRELATION_ID -if ($null -ne $commandCorrelationId) -{ - [System.Environment]::SetEnvironmentVariable("COMMAND_CORRELATION_ID", $null) -} - -$IssueSources = @{ - CustomerScript = "CustomerScript" - TaskInternal = "TaskInternal" -} - -<# -.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-AddAttachment { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Type, - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$Path, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'addattachment' -Data $Path -Properties @{ - 'type' = $Type - 'name' = $Name - } -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-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. -#> -function Write-AddBuildTag { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Value, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'build' -Event 'addbuildtag' -Data $Value -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-AssociateArtifact { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$Type, - [hashtable]$Properties, - [switch]$AsOutput) - - $p = @{ } - if ($Properties) { - foreach ($key in $Properties.Keys) { - $p[$key] = $Properties[$key] - } - } - - $p['artifactname'] = $Name - $p['artifacttype'] = $Type - Write-LoggingCommand -Area 'artifact' -Event 'associate' -Data $Path -Properties $p -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-LogDetail { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [guid]$Id, - $ParentId, - [string]$Type, - [string]$Name, - $Order, - $StartTime, - $FinishTime, - $Progress, - [ValidateSet('Unknown', 'Initialized', 'InProgress', 'Completed')] - [Parameter()] - $State, - [ValidateSet('Succeeded', 'SucceededWithIssues', 'Failed', 'Cancelled', 'Skipped')] - [Parameter()] - $Result, - [string]$Message, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'logdetail' -Data $Message -Properties @{ - 'id' = $Id - 'parentid' = $ParentId - 'type' = $Type - 'name' = $Name - 'order' = $Order - 'starttime' = $StartTime - 'finishtime' = $FinishTime - 'progress' = $Progress - 'state' = $State - 'result' = $Result - } -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-SetProgress { - [CmdletBinding()] - param( - [ValidateRange(0, 100)] - [Parameter(Mandatory = $true)] - [int]$Percent, - [string]$CurrentOperation, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'setprogress' -Data $CurrentOperation -Properties @{ - 'value' = $Percent - } -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-SetResult { - [CmdletBinding(DefaultParameterSetName = 'AsOutput')] - param( - [ValidateSet("Succeeded", "SucceededWithIssues", "Failed", "Cancelled", "Skipped")] - [Parameter(Mandatory = $true)] - [string]$Result, - [string]$Message, - [Parameter(ParameterSetName = 'AsOutput')] - [switch]$AsOutput, - [Parameter(ParameterSetName = 'DoNotThrow')] - [switch]$DoNotThrow) - - Write-LoggingCommand -Area 'task' -Event 'complete' -Data $Message -Properties @{ - 'result' = $Result - } -AsOutput:$AsOutput - if ($Result -eq 'Failed' -and !$AsOutput -and !$DoNotThrow) { - # Special internal exception type to control the flow. Not currently intended - # for public usage and subject to change. - throw (New-Object VstsTaskSdk.TerminationException($Message)) - } -} - -<# -.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-SetSecret { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Value, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'setsecret' -Data $Value -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-SetVariable { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [string]$Value, - [switch]$Secret, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $Value -Properties @{ - 'variable' = $Name - 'issecret' = $Secret - } -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-TaskDebug { - [CmdletBinding()] - param( - [string]$Message, - [switch]$AsOutput) - - Write-TaskDebug_Internal @PSBoundParameters -} - -<# -.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-TaskError { - [CmdletBinding()] - param( - [string]$Message, - [string]$ErrCode, - [string]$SourcePath, - [string]$LineNumber, - [string]$ColumnNumber, - [switch]$AsOutput, - [string]$IssueSource, - [string]$AuditAction - ) - - Write-LogIssue -Type error @PSBoundParameters -} - -<# -.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-TaskVerbose { - [CmdletBinding()] - param( - [string]$Message, - [switch]$AsOutput) - - Write-TaskDebug_Internal @PSBoundParameters -AsVerbose -} - -<# -.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-TaskWarning { - [CmdletBinding()] - param( - [string]$Message, - [string]$ErrCode, - [string]$SourcePath, - [string]$LineNumber, - [string]$ColumnNumber, - [switch]$AsOutput, - [string]$IssueSource, - [string]$AuditAction - ) - - Write-LogIssue -Type warning @PSBoundParameters -} - -<# -.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. -#> -function Write-UpdateBuildNumber { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Value, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'build' -Event 'updatebuildnumber' -Data $Value -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-UploadArtifact { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$ContainerFolder, - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$Path, - [switch]$AsOutput) - - Write-LoggingCommand -Area 'artifact' -Event 'upload' -Data $Path -Properties @{ - 'containerfolder' = $ContainerFolder - 'artifactname' = $Name - } -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-UploadBuildLog { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [switch]$AsOutput) - - 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 -} - -<# -.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-LoggingCommand { - [CmdletBinding(DefaultParameterSetName = 'Parameters')] - param( - [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] - [string]$Area, - [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] - [string]$Event, - [Parameter(ParameterSetName = 'Parameters')] - [string]$Data, - [Parameter(ParameterSetName = 'Parameters')] - [hashtable]$Properties, - [Parameter(Mandatory = $true, ParameterSetName = 'Object')] - $Command, - [switch]$AsOutput) - - if ($PSCmdlet.ParameterSetName -eq 'Object') { - Write-LoggingCommand -Area $Command.Area -Event $Command.Event -Data $Command.Data -Properties $Command.Properties -AsOutput:$AsOutput - return - } - - $command = Format-LoggingCommand -Area $Area -Event $Event -Data $Data -Properties $Properties - if ($AsOutput) { - $command - } else { - Write-Host $command - } -} - -######################################## -# Private functions. -######################################## -function Format-LoggingCommandData { - [CmdletBinding()] - param([string]$Value, [switch]$Reverse) - - if (!$Value) { - return '' - } - - if (!$Reverse) { - foreach ($mapping in $script:loggingCommandEscapeMappings) { - $Value = $Value.Replace($mapping.Token, $mapping.Replacement) - } - } else { - for ($i = $script:loggingCommandEscapeMappings.Length - 1 ; $i -ge 0 ; $i--) { - $mapping = $script:loggingCommandEscapeMappings[$i] - $Value = $Value.Replace($mapping.Replacement, $mapping.Token) - } - } - - return $Value -} - -function Format-LoggingCommand { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Area, - [Parameter(Mandatory = $true)] - [string]$Event, - [string]$Data, - [System.Collections.IDictionary]$Properties) - - # Append the preamble. - [System.Text.StringBuilder]$sb = New-Object -TypeName System.Text.StringBuilder - $null = $sb.Append($script:loggingCommandPrefix).Append($Area).Append('.').Append($Event) - - # Append the properties. - if ($Properties) { - $first = $true - foreach ($key in $Properties.Keys) { - [string]$value = Format-LoggingCommandData $Properties[$key] - if ($value) { - if ($first) { - $null = $sb.Append(' ') - $first = $false - } else { - $null = $sb.Append(';') - } - - $null = $sb.Append("$key=$value") - } - } - } - - # Append the tail and output the value. - $Data = Format-LoggingCommandData $Data - $sb.Append(']').Append($Data).ToString() -} - -function Write-LogIssue { - [CmdletBinding()] - param( - [ValidateSet('warning', 'error')] - [Parameter(Mandatory = $true)] - [string]$Type, - [string]$Message, - [string]$ErrCode, - [string]$SourcePath, - [string]$LineNumber, - [string]$ColumnNumber, - [switch]$AsOutput, - [AllowNull()] - [ValidateSet('CustomerScript', 'TaskInternal')] - [string]$IssueSource = $IssueSources.TaskInternal, - [string]$AuditAction - ) - - $properties = [ordered]@{ - 'type' = $Type - 'code' = $ErrCode - 'sourcepath' = $SourcePath - 'linenumber' = $LineNumber - 'columnnumber' = $ColumnNumber - 'source' = $IssueSource - 'correlationId' = $commandCorrelationId - 'auditAction' = $AuditAction - } - $command = Format-LoggingCommand -Area 'task' -Event 'logissue' -Data $Message -Properties $properties - if ($AsOutput) { - return $command - } - - if ($Type -eq 'error') { - $foregroundColor = $host.PrivateData.ErrorForegroundColor - $backgroundColor = $host.PrivateData.ErrorBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::Red - $backgroundColor = [System.ConsoleColor]::Black - } - } else { - $foregroundColor = $host.PrivateData.WarningForegroundColor - $backgroundColor = $host.PrivateData.WarningBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::Yellow - $backgroundColor = [System.ConsoleColor]::Black - } - } - - Write-Host $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor -} - -function Write-TaskDebug_Internal { - [CmdletBinding()] - param( - [string]$Message, - [switch]$AsVerbose, - [switch]$AsOutput) - - $command = Format-LoggingCommand -Area 'task' -Event 'debug' -Data $Message - if ($AsOutput) { - return $command - } - - if ($AsVerbose) { - $foregroundColor = $host.PrivateData.VerboseForegroundColor - $backgroundColor = $host.PrivateData.VerboseBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::Cyan - $backgroundColor = [System.ConsoleColor]::Black - } - } else { - $foregroundColor = $host.PrivateData.DebugForegroundColor - $backgroundColor = $host.PrivateData.DebugBackgroundColor - if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { - $foregroundColor = [System.ConsoleColor]::DarkGray - $backgroundColor = [System.ConsoleColor]::Black - } - } - - Write-Host -Object $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor -} diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 deleted file mode 100644 index 51cda34..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/LongPathFunctions.ps1 +++ /dev/null @@ -1,205 +0,0 @@ -######################################## -# Private functions. -######################################## -function ConvertFrom-LongFormPath { - [CmdletBinding()] - param([string]$Path) - - if ($Path) { - if ($Path.StartsWith('\\?\UNC')) { - # E.g. \\?\UNC\server\share -> \\server\share - return $Path.Substring(1, '\?\UNC'.Length) - } elseif ($Path.StartsWith('\\?\')) { - # E.g. \\?\C:\directory -> C:\directory - return $Path.Substring('\\?\'.Length) - } - } - - return $Path -} -function ConvertTo-LongFormPath { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path) - - [string]$longFormPath = Get-FullNormalizedPath -Path $Path - if ($longFormPath -and !$longFormPath.StartsWith('\\?')) { - if ($longFormPath.StartsWith('\\')) { - # E.g. \\server\share -> \\?\UNC\server\share - return "\\?\UNC$($longFormPath.Substring(1))" - } else { - # E.g. C:\directory -> \\?\C:\directory - return "\\?\$longFormPath" - } - } - - return $longFormPath -} - -# TODO: ADD A SWITCH TO EXCLUDE FILES, A SWITCH TO EXCLUDE DIRECTORIES, AND A SWITCH NOT TO FOLLOW REPARSE POINTS. -function Get-DirectoryChildItem { - [CmdletBinding()] - param( - [string]$Path, - [ValidateNotNullOrEmpty()] - [Parameter()] - [string]$Filter = "*", - [switch]$Force, - [VstsTaskSdk.FS.FindFlags]$Flags = [VstsTaskSdk.FS.FindFlags]::LargeFetch, - [VstsTaskSdk.FS.FindInfoLevel]$InfoLevel = [VstsTaskSdk.FS.FindInfoLevel]::Basic, - [switch]$Recurse) - - $stackOfDirectoryQueues = New-Object System.Collections.Stack - while ($true) { - $directoryQueue = New-Object System.Collections.Queue - $fileQueue = New-Object System.Collections.Queue - $findData = New-Object VstsTaskSdk.FS.FindData - $longFormPath = (ConvertTo-LongFormPath $Path) - $handle = $null - try { - $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( - [System.IO.Path]::Combine($longFormPath, $Filter), - $InfoLevel, - $findData, - [VstsTaskSdk.FS.FindSearchOps]::NameMatch, - [System.IntPtr]::Zero, - $Flags) - if (!$handle.IsInvalid) { - while ($true) { - if ($findData.fileName -notin '.', '..') { - $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes - # If the item is hidden, check if $Force is specified. - if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { - # Create the item. - $item = New-Object -TypeName psobject -Property @{ - 'Attributes' = $attributes - 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) - 'Name' = $findData.fileName - } - # Output directories immediately. - if ($item.Attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { - $item - # Append to the directory queue if recursive and default filter. - if ($Recurse -and $Filter -eq '*') { - $directoryQueue.Enqueue($item) - } - } else { - # Hold the files until all directories have been output. - $fileQueue.Enqueue($item) - } - } - } - - if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } - - if ($handle.IsInvalid) { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path - )) - } - } - } - } finally { - if ($handle -ne $null) { $handle.Dispose() } - } - - # If recursive and non-default filter, queue child directories. - if ($Recurse -and $Filter -ne '*') { - $findData = New-Object VstsTaskSdk.FS.FindData - $handle = $null - try { - $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( - [System.IO.Path]::Combine($longFormPath, '*'), - [VstsTaskSdk.FS.FindInfoLevel]::Basic, - $findData, - [VstsTaskSdk.FS.FindSearchOps]::NameMatch, - [System.IntPtr]::Zero, - $Flags) - if (!$handle.IsInvalid) { - while ($true) { - if ($findData.fileName -notin '.', '..') { - $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes - # If the item is hidden, check if $Force is specified. - if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { - # Collect directories only. - if ($attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { - # Create the item. - $item = New-Object -TypeName psobject -Property @{ - 'Attributes' = $attributes - 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) - 'Name' = $findData.fileName - } - $directoryQueue.Enqueue($item) - } - } - } - - if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } - - if ($handle.IsInvalid) { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path - )) - } - } - } - } finally { - if ($handle -ne $null) { $handle.Dispose() } - } - } - - # Output the files. - $fileQueue - - # Push the directory queue onto the stack if any directories were found. - if ($directoryQueue.Count) { $stackOfDirectoryQueues.Push($directoryQueue) } - - # Break out of the loop if no more directory queues to process. - if (!$stackOfDirectoryQueues.Count) { break } - - # Get the next path. - $directoryQueue = $stackOfDirectoryQueues.Peek() - $Path = $directoryQueue.Dequeue().FullName - - # Pop the directory queue if it's empty. - if (!$directoryQueue.Count) { $null = $stackOfDirectoryQueues.Pop() } - } -} - -function Get-FullNormalizedPath { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path) - - [string]$outPath = $Path - [uint32]$bufferSize = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, 0, $null, $null) - [int]$lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - if ($bufferSize -gt 0) { - $absolutePath = New-Object System.Text.StringBuilder([int]$bufferSize) - [uint32]$length = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, $bufferSize, $absolutePath, $null) - $lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - if ($length -gt 0) { - $outPath = $absolutePath.ToString() - } else { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - $lastWin32Error - Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path - )) - } - } else { - throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( - $lastWin32Error - Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path - )) - } - - if ($outPath.EndsWith('\') -and !$outPath.EndsWith(':\')) { - $outPath = $outPath.TrimEnd('\') - } - - $outPath -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Minimatch.dll b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Minimatch.dll deleted file mode 100644 index 700ddc426e56b08270c9dcbb1ade8ded248f98d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18432 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 X?dtd6e<)43|K{fYzxw~TJn(-2*CUr_ diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/OutFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/OutFunctions.ps1 deleted file mode 100644 index ce9160b..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/OutFunctions.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -# TODO: It would be better if the Out-Default function resolved the underlying Out-Default -# command in the begin block. This would allow for supporting other modules that override -# Out-Default. -$script:outDefaultCmdlet = $ExecutionContext.InvokeCommand.GetCmdlet("Microsoft.PowerShell.Core\Out-Default") - -######################################## -# Public functions. -######################################## -function Out-Default { - [CmdletBinding(ConfirmImpact = "Medium")] - param( - [Parameter(ValueFromPipeline = $true)] - [System.Management.Automation.PSObject]$InputObject) - - begin { - #Write-Host '[Entering Begin Out-Default]' - $__sp = { & $script:outDefaultCmdlet @PSBoundParameters }.GetSteppablePipeline() - $__sp.Begin($pscmdlet) - #Write-Host '[Leaving Begin Out-Default]' - } - - process { - #Write-Host '[Entering Process Out-Default]' - if ($_ -is [System.Management.Automation.ErrorRecord]) { - Write-Verbose -Message 'Error record:' 4>&1 | Out-Default - Write-Verbose -Message (Remove-TrailingNewLine (Out-String -InputObject $_ -Width 2147483647)) 4>&1 | Out-Default - Write-Verbose -Message 'Script stack trace:' 4>&1 | Out-Default - Write-Verbose -Message "$($_.ScriptStackTrace)" 4>&1 | Out-Default - Write-Verbose -Message 'Exception:' 4>&1 | Out-Default - Write-Verbose -Message $_.Exception.ToString() 4>&1 | Out-Default - Write-TaskError -Message $_.Exception.Message -IssueSource $IssueSources.TaskInternal - } elseif ($_ -is [System.Management.Automation.WarningRecord]) { - Write-TaskWarning -Message (Remove-TrailingNewLine (Out-String -InputObject $_ -Width 2147483647)) -IssueSource $IssueSources.TaskInternal - } elseif ($_ -is [System.Management.Automation.VerboseRecord] -and !$global:__vstsNoOverrideVerbose) { - foreach ($private:str in (Format-DebugMessage -Object $_)) { - Write-TaskVerbose -Message $private:str - } - } elseif ($_ -is [System.Management.Automation.DebugRecord] -and !$global:__vstsNoOverrideVerbose) { - foreach ($private:str in (Format-DebugMessage -Object $_)) { - Write-TaskDebug -Message $private:str - } - } else { -# TODO: Consider using out-string here to control the width. As a security precaution it would actually be best to set it to max so wrapping doesn't interfere with secret masking. - $__sp.Process($_) - } - - #Write-Host '[Leaving Process Out-Default]' - } - - end { - #Write-Host '[Entering End Out-Default]' - $__sp.End() - #Write-Host '[Leaving End Out-Default]' - } -} - -######################################## -# Private functions. -######################################## -function Format-DebugMessage { - [CmdletBinding()] - param([psobject]$Object) - - $private:str = Out-String -InputObject $Object -Width 2147483647 - $private:str = Remove-TrailingNewLine $private:str - "$private:str".Replace("`r`n", "`n").Replace("`r", "`n").Split("`n"[0]) -} - -function Remove-TrailingNewLine { - [CmdletBinding()] - param($Str) - if ([object]::ReferenceEquals($Str, $null)) { - return $Str - } elseif ($Str.EndsWith("`r`n")) { - return $Str.Substring(0, $Str.Length - 2) - } else { - return $Str - } -} diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml deleted file mode 100644 index d930995..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/PSGetModuleInfo.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - Microsoft.PowerShell.Commands.PSRepositoryItemInfo - System.Management.Automation.PSCustomObject - System.Object - - - VstsTaskSdk - 0.21.0 - Module - VSTS Task SDK - Microsoft Corporation - - - System.Object[] - System.Array - System.Object - - - VSTS - martinmrazik - EDergachev - tramsing - - - (c) Microsoft Corporation. All rights reserved. -

2024-05-02T10:19:39-06:00
- - - - https://github.com/Microsoft/azure-pipelines-task-lib - - - - - PSModule - - - - - System.Collections.Hashtable - System.Object - - - - Command - - - - - - - DscResource - - - - RoleCapability - - - - Function - - - - Workflow - - - - Cmdlet - - - - - - - - - - - https://www.powershellgallery.com/api/v2 - PSGallery - NuGet - - - System.Management.Automation.PSCustomObject - System.Object - - - (c) Microsoft Corporation. All rights reserved. - VSTS Task SDK - False - True - True - 88436 - 684804 - 67529 - 5/2/2024 10:19:39 -06:00 - 5/2/2024 10:19:39 -06:00 - 5/22/2025 21:30:00 -06:00 - PSModule - False - 2025-05-22T21:30:00Z - 0.21.0 - Microsoft Corporation - false - Module - VstsTaskSdk.nuspec|FindFunctions.ps1|LegacyFindFunctions.ps1|LocalizationFunctions.ps1|LongPathFunctions.ps1|OutFunctions.ps1|ToolFunctions.ps1|VstsTaskSdk.dll|VstsTaskSdk.psd1|Strings\resources.resjson\de-DE\resources.resjson|Strings\resources.resjson\es-ES\resources.resjson|Strings\resources.resjson\it-IT\resources.resjson|Strings\resources.resjson\ja-JP\resources.resjson|Strings\resources.resjson\ko-KR\resources.resjson|Strings\resources.resjson\ru-RU\resources.resjson|Strings\resources.resjson\zh-CN\resources.resjson|Strings\resources.resjson\zh-TW\resources.resjson|InputFunctions.ps1|lib.json|LoggingCommandFunctions.ps1|Minimatch.dll|ServerOMFunctions.ps1|TraceFunctions.ps1|VstsTaskSdk.psm1|Strings\resources.resjson\en-US\resources.resjson|Strings\resources.resjson\fr-FR\resources.resjson - bbed04e2-4e8e-4089-90a2-58b858fed8d8 - 3.0 - Microsoft Corporation - - - D:\Work\GitHub\bcdevopsextension\temp\VstsTaskSdk\0.21.0 - - - diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 deleted file mode 100644 index 6fd19ea..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1 +++ /dev/null @@ -1,684 +0,0 @@ -<# -.SYNOPSIS -Gets assembly reference information. - -.DESCRIPTION -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. - -Walks an assembly's references to determine all of it's dependencies. Also walks the references of the dependencies, and so on until all nested dependencies have been traversed. Dependencies are searched for in the directory of the specified assembly. NET Framework assemblies are omitted. - -See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER LiteralPath -Assembly to walk. - -.EXAMPLE -Get-VstsAssemblyReference -LiteralPath C:\nuget\microsoft.teamfoundationserver.client.14.102.0\lib\net45\Microsoft.TeamFoundation.Build2.WebApi.dll -#> -function Get-AssemblyReference { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$LiteralPath) - - $ErrorActionPreference = 'Stop' - 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) - $hashtable = @{ } - $queue = @( [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($LiteralPath).GetName() ) - while ($queue.Count) { - # Add a blank line between assemblies. - Write-Output '' - - # Pop. - $assemblyName = $queue[0] - $queue = @( $queue | Select-Object -Skip 1 ) - - # Attempt to find the assembly in the same directory. - $assembly = $null - $path = "$directory\$($assemblyName.Name).dll" - if ((Test-Path -LiteralPath $path -PathType Leaf)) { - $assembly = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($path) - } else { - $path = "$directory\$($assemblyName.Name).exe" - if ((Test-Path -LiteralPath $path -PathType Leaf)) { - $assembly = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($path) - } - } - - # Make sure the assembly full name matches, not just the file name. - if ($assembly -and $assembly.GetName().FullName -ne $assemblyName.FullName) { - $assembly = $null - } - - # Print the assembly. - if ($assembly) { - Write-Output $assemblyName.FullName - } else { - if ($assemblyName.FullName -eq 'Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed') { - Write-Warning "*** NOT FOUND $($assemblyName.FullName) *** This is an expected condition when using the HTTP clients from the 15.x VSTS REST SDK. Use Get-VstsVssHttpClient to load the HTTP clients (which applies a binding redirect assembly resolver for Newtonsoft.Json). Otherwise you will need to manage the binding redirect yourself." - } else { - Write-Warning "*** NOT FOUND $($assemblyName.FullName) ***" - } - - continue - } - - # Walk the references. - $refAssemblyNames = @( $assembly.GetReferencedAssemblies() ) - for ($i = 0 ; $i -lt $refAssemblyNames.Count ; $i++) { - $refAssemblyName = $refAssemblyNames[$i] - - # Skip framework assemblies. - $fxPaths = @( - "$env:windir\Microsoft.Net\Framework64\v4.0.30319\$($refAssemblyName.Name).dll" - "$env:windir\Microsoft.Net\Framework64\v4.0.30319\WPF\$($refAssemblyName.Name).dll" - ) - $fxPath = $fxPaths | - Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | - Where-Object { [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($_).GetName().FullName -eq $refAssemblyName.FullName } - if ($fxPath) { - continue - } - - # Print the reference. - Write-Output " $($refAssemblyName.FullName)" - - # Add new references to the queue. - if (!$hashtable[$refAssemblyName.FullName]) { - $queue += $refAssemblyName - $hashtable[$refAssemblyName.FullName] = $true - } - } - } -} - -<# -.SYNOPSIS -Gets a credentials object that can be used with the TFS extended client SDK. - -.DESCRIPTION -The agent job token is used 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). - -Refer to Get-VstsTfsService for a more simple to get a TFS service object. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER OMDirectory -Directory where the extended client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.EXAMPLE -# -# Refer to Get-VstsTfsService for a more simple way to get a TFS service object. -# -$credentials = Get-VstsTfsClientCredentials -Add-Type -LiteralPath "$PSScriptRoot\Microsoft.TeamFoundation.VersionControl.Client.dll" -$tfsTeamProjectCollection = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection( - (Get-VstsTaskVariable -Name 'System.TeamFoundationCollectionUri' -Require), - $credentials) -$versionControlServer = $tfsTeamProjectCollection.GetService([Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer]) -$versionControlServer.GetItems('$/*').Items | Format-List -#> -function Get-TfsClientCredentials { - [CmdletBinding()] - param([string]$OMDirectory) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Get the endpoint. - $endpoint = Get-Endpoint -Name SystemVssConnection -Require - - # Test if the Newtonsoft.Json DLL exists in the OM directory. - $newtonsoftDll = [System.IO.Path]::Combine($OMDirectory, "Newtonsoft.Json.dll") - Write-Verbose "Testing file path: '$newtonsoftDll'" - if (!(Test-Path -LiteralPath $newtonsoftDll -PathType Leaf)) { - Write-Verbose 'Not found. Rethrowing exception.' - throw - } - - # Add a binding redirect and try again. Parts of the Dev15 preview SDK have a - # 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] { - param($sender, $e) - - if ($e.Name -like 'Newtonsoft.Json, *') { - Write-Verbose "Resolving '$($e.Name)' to '$newtonsoftDll'." - - return [System.Reflection.Assembly]::LoadFrom($newtonsoftDll) - } - - return $null - } - [System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolve) - - # Validate the type can be found. - $null = Get-OMType -TypeName 'Microsoft.TeamFoundation.Client.TfsClientCredentials' -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require - - # Construct the credentials. - $credentials = New-Object Microsoft.TeamFoundation.Client.TfsClientCredentials($false) # Do not use default credentials. - $credentials.AllowInteractive = $false - $credentials.Federated = New-Object Microsoft.TeamFoundation.Client.OAuthTokenCredential([string]$endpoint.auth.parameters.AccessToken) - return $credentials - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Gets a TFS extended client service. - -.DESCRIPTION -Gets an instance of an ITfsTeamProjectCollectionObject. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER TypeName -Namespace-qualified type name of the service to get. - -.PARAMETER OMDirectory -Directory where the extended client object model DLLs are located. If the DLLs for the types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. - -.PARAMETER Uri -URI to use when initializing the service. If not specified, defaults to System.TeamFoundationCollectionUri. - -.PARAMETER TfsClientCredentials -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 -$versionControlServer.GetItems('$/*').Items | Format-List -#> -function Get-TfsService { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TypeName, - - [string]$OMDirectory, - - [string]$Uri, - - $TfsClientCredentials) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Default the URI to the collection URI. - if (!$Uri) { - $Uri = Get-TaskVariable -Name System.TeamFoundationCollectionUri -Require - } - - # Default the credentials. - if (!$TfsClientCredentials) { - $TfsClientCredentials = Get-TfsClientCredentials -OMDirectory $OMDirectory - } - - # Validate the project collection type can be loaded. - $null = Get-OMType -TypeName 'Microsoft.TeamFoundation.Client.TfsTeamProjectCollection' -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require - - # Load the project collection object. - $tfsTeamProjectCollection = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection($Uri, $TfsClientCredentials) - - # Validate the requested type can be loaded. - $type = Get-OMType -TypeName $TypeName -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require - - # Return the service object. - return $tfsTeamProjectCollection.GetService($type) - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Gets a credentials object that can be used with the VSTS REST SDK. - -.DESCRIPTION -The agent job token is used 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 service build/release identity). - -Refer to Get-VstsVssHttpClient for a more simple to get a VSS HTTP client. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -.PARAMETER OMDirectory -Directory where the REST client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -.EXAMPLE -# -# Refer to Get-VstsTfsService for a more simple way to get a TFS service object. -# -# This example works using the 14.x .NET SDK. A Newtonsoft.Json 6.0 to 8.0 binding -# redirect may be required when working with the 15.x SDK. Or use Get-VstsVssHttpClient -# to avoid managing the binding redirect. -# -$vssCredentials = Get-VstsVssCredentials -$collectionUrl = New-Object System.Uri((Get-VstsTaskVariable -Name 'System.TeamFoundationCollectionUri' -Require)) -Add-Type -LiteralPath "$PSScriptRoot\Microsoft.TeamFoundation.Core.WebApi.dll" -$projectHttpClient = New-Object Microsoft.TeamFoundation.Core.WebApi.ProjectHttpClient($collectionUrl, $vssCredentials) -$projectHttpClient.GetProjects().Result -#> -function Get-VssCredentials { - [CmdletBinding()] - param([string]$OMDirectory) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Get the endpoint. - $endpoint = Get-Endpoint -Name SystemVssConnection -Require - - # Check if the VssOAuthAccessTokenCredential type is available. - if ((Get-OMType -TypeName 'Microsoft.VisualStudio.Services.OAuth.VssOAuthAccessTokenCredential' -OMKind 'WebApi' -OMDirectory $OMDirectory)) { - # Create the federated credential. - $federatedCredential = New-Object Microsoft.VisualStudio.Services.OAuth.VssOAuthAccessTokenCredential($endpoint.auth.parameters.AccessToken) - } else { - # Validate the fallback type can be loaded. - $null = Get-OMType -TypeName 'Microsoft.VisualStudio.Services.Client.VssOAuthCredential' -OMKind 'WebApi' -OMDirectory $OMDirectory -Require - - # Create the federated credential. - $federatedCredential = New-Object Microsoft.VisualStudio.Services.Client.VssOAuthCredential($endpoint.auth.parameters.AccessToken) - } - - # Return the credentials. - return New-Object Microsoft.VisualStudio.Services.Common.VssCredentials( - (New-Object Microsoft.VisualStudio.Services.Common.WindowsCredential($false)), # Do not use default credentials. - $federatedCredential, - [Microsoft.VisualStudio.Services.Common.CredentialPromptType]::DoNotPrompt) - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.SYNOPSIS -Gets a VSS HTTP client. - -.DESCRIPTION -Gets an instance of an VSS HTTP client. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -.PARAMETER TypeName -Namespace-qualified type name of the HTTP client to get. - -.PARAMETER OMDirectory -Directory where the REST client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. - -If not specified, defaults to the directory of the entry script for the task. - -*** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/azure-pipelines-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. - -# .PARAMETER Uri -# URI to use when initializing the HTTP client. If not specified, defaults to System.TeamFoundationCollectionUri. - -# .PARAMETER VssCredentials -# 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 -$projectHttpClient.GetProjects().Result -#> -function Get-VssHttpClient { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TypeName, - - [string]$OMDirectory, - - [string]$Uri, - - $VssCredentials, - - $WebProxy = (Get-WebProxy), - - $ClientCert = (Get-ClientCertificate), - - [switch]$IgnoreSslError) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - $originalErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = 'Stop' - - # Default the URI to the collection URI. - if (!$Uri) { - $Uri = Get-TaskVariable -Name System.TeamFoundationCollectionUri -Require - } - - # Cast the URI. - [uri]$Uri = New-Object System.Uri($Uri) - - # Default the credentials. - if (!$VssCredentials) { - $VssCredentials = Get-VssCredentials -OMDirectory $OMDirectory - } - - # 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, $Settings) - } catch { - # Rethrow if the exception is not due to Newtonsoft.Json DLL not found. - if ($_.Exception.InnerException -isnot [System.IO.FileNotFoundException] -or - $_.Exception.InnerException.FileName -notlike 'Newtonsoft.Json, *') { - - throw - } - - # Default the OMDirectory to the directory of the entry script for the task. - if (!$OMDirectory) { - $OMDirectory = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") - Write-Verbose "Defaulted OM directory to: '$OMDirectory'" - } - - # Test if the Newtonsoft.Json DLL exists in the OM directory. - $newtonsoftDll = [System.IO.Path]::Combine($OMDirectory, "Newtonsoft.Json.dll") - Write-Verbose "Testing file path: '$newtonsoftDll'" - if (!(Test-Path -LiteralPath $newtonsoftDll -PathType Leaf)) { - Write-Verbose 'Not found. Rethrowing exception.' - throw - } - - # Add a binding redirect and try again. Parts of the Dev15 preview SDK have a - # 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] { - param($sender, $e) - - if ($e.Name -like 'Newtonsoft.Json, *') { - Write-Verbose "Resolving '$($e.Name)'" - return [System.Reflection.Assembly]::LoadFrom($newtonsoftDll) - } - - Write-Verbose "Unable to resolve assembly name '$($e.Name)'" - return $null - } - [System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolve) - try { - # Try again to construct the HTTP client. - Write-Verbose "Trying again to construct the HTTP client." - return New-Object $TypeName($Uri, $VssCredentials, $Settings) - } finally { - # Unregister the assembly resolver. - Write-Verbose "Removing assemlby resolver." - [System.AppDomain]::CurrentDomain.remove_AssemblyResolve($onAssemblyResolve) - } - } - } catch { - $ErrorActionPreference = $originalErrorActionPreference - Write-Error $_ - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} - -<# -.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/azure-pipelines-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. -######################################## -function Get-OMType { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TypeName, - - [ValidateSet('ExtendedClient', 'WebApi')] - [Parameter(Mandatory = $true)] - [string]$OMKind, - - [string]$OMDirectory, - - [switch]$Require) - - Trace-EnteringInvocation -InvocationInfo $MyInvocation - try { - # Default the OMDirectory to the directory of the entry script for the task. - if (!$OMDirectory) { - $OMDirectory = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") - Write-Verbose "Defaulted OM directory to: '$OMDirectory'" - } - - # Try to load the type. - $errorRecord = $null - Write-Verbose "Testing whether type can be loaded: '$TypeName'" - $ErrorActionPreference = 'Ignore' - try { - # Failure when attempting to cast a string to a type, transfers control to the - # catch handler even when the error action preference is ignore. The error action - # is set to Ignore so the $Error variable is not polluted. - $type = [type]$TypeName - - # Success. - Write-Verbose "The type was loaded successfully." - return $type - } catch { - # Store the error record. - $errorRecord = $_ - } - - $ErrorActionPreference = 'Stop' - Write-Verbose "The type was not loaded." - - # Build a list of candidate DLL file paths from the namespace. - $dllPaths = @( ) - $namespace = $TypeName - while ($namespace.LastIndexOf('.') -gt 0) { - # Trim the next segment from the namespace. - $namespace = $namespace.SubString(0, $namespace.LastIndexOf('.')) - - # Derive potential DLL file paths based on the namespace and OM kind (i.e. extended client vs web API). - if ($OMKind -eq 'ExtendedClient') { - if ($namespace -like 'Microsoft.TeamFoundation.*') { - $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.dll") - } - } else { - if ($namespace -like 'Microsoft.TeamFoundation.*' -or - $namespace -like 'Microsoft.VisualStudio.Services' -or - $namespace -like 'Microsoft.VisualStudio.Services.*') { - - $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.WebApi.dll") - $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.dll") - } - } - } - - foreach ($dllPath in $dllPaths) { - # Check whether the DLL exists. - Write-Verbose "Testing leaf path: '$dllPath'" - if (!(Test-Path -PathType Leaf -LiteralPath "$dllPath")) { - Write-Verbose "Not found." - continue - } - - # Load the DLL. - Write-Verbose "Loading assembly: $dllPath" - try { - Add-Type -LiteralPath $dllPath - } catch { - # Write the information to the verbose stream and proceed to attempt to load the requested type. - # - # The requested type may successfully load now. For example, the type used with the 14.0 Web API for the - # federated credential (VssOAuthCredential) resides in Microsoft.VisualStudio.Services.Client.dll. Even - # though loading the DLL results in a ReflectionTypeLoadException when Microsoft.ServiceBus.dll (approx 3.75mb) - # is not present, enough types are loaded to use the VssOAuthCredential federated credential with the Web API - # HTTP clients. - Write-Verbose "$($_.Exception.GetType().FullName): $($_.Exception.Message)" - if ($_.Exception -is [System.Reflection.ReflectionTypeLoadException]) { - for ($i = 0 ; $i -lt $_.Exception.LoaderExceptions.Length ; $i++) { - $loaderException = $_.Exception.LoaderExceptions[$i] - Write-Verbose "LoaderExceptions[$i]: $($loaderException.GetType().FullName): $($loaderException.Message)" - } - } - } - - # Try to load the type. - Write-Verbose "Testing whether type can be loaded: '$TypeName'" - $ErrorActionPreference = 'Ignore' - try { - # Failure when attempting to cast a string to a type, transfers control to the - # catch handler even when the error action preference is ignore. The error action - # is set to Ignore so the $Error variable is not polluted. - $type = [type]$TypeName - - # Success. - Write-Verbose "The type was loaded successfully." - return $type - } catch { - $errorRecord = $_ - } - - $ErrorActionPreference = 'Stop' - Write-Verbose "The type was not loaded." - } - - # Check whether to propagate the error. - if ($Require) { - Write-Error $errorRecord - } - } finally { - Trace-LeavingInvocation -InvocationInfo $MyInvocation - } -} diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson deleted file mode 100644 index 248b674..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-DE/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Fehler beim Aufzählen von Unterverzeichnissen für den folgenden Pfad: \"{0}\"", - "loc.messages.PSLIB_FileNotFound0": "Die Datei wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_Input0": "\"{0}\"-Eingabe", - "loc.messages.PSLIB_InvalidPattern0": "Ungültiges Muster: \"{0}\"", - "loc.messages.PSLIB_LeafPathNotFound0": "Der Blattpfad wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Fehler bei der Normalisierung bzw. Erweiterung des Pfads. Die Pfadlänge wurde vom Kernel32-Subsystem nicht zurückgegeben für: \"{0}\"", - "loc.messages.PSLIB_PathNotFound0": "Der Pfad wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Der Prozess \"{0}\" wurde mit dem Code \"{1}\" beendet.", - "loc.messages.PSLIB_Required0": "Erforderlich: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Fehler beim Zeichenfolgenformat.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "Der Zeichenfolgen-Ressourcenschlüssel wurde nicht gefunden: \"{0}\".", - "loc.messages.PSLIB_TaskVariable0": "\"{0}\"-Taskvariable" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson deleted file mode 100644 index 66c17bc..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Enumerating subdirectories failed for path: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "File not found: '{0}'", - "loc.messages.PSLIB_Input0": "'{0}' input", - "loc.messages.PSLIB_InvalidPattern0": "Invalid pattern: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Leaf path not found: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Path normalization/expansion failed. The path length was not returned by the Kernel32 subsystem for: '{0}'", - "loc.messages.PSLIB_PathNotFound0": "Path not found: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Process '{0}' exited with code '{1}'.", - "loc.messages.PSLIB_Required0": "Required: {0}", - "loc.messages.PSLIB_StringFormatFailed": "String format failed.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "String resource key not found: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' task variable" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson deleted file mode 100644 index b79ac21..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-ES/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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}'", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "No se pudieron enumerar los subdirectorios de la ruta de acceso: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "Archivo no encontrado: '{0}'", - "loc.messages.PSLIB_Input0": "Entrada '{0}'", - "loc.messages.PSLIB_InvalidPattern0": "Patrón no válido: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "No se encuentra la ruta de acceso de la hoja: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "No se pudo normalizar o expandir la ruta de acceso. El subsistema Kernel32 no devolvió la longitud de la ruta de acceso para: '{0}'", - "loc.messages.PSLIB_PathNotFound0": "No se encuentra la ruta de acceso: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "El proceso '{0}' finalizó con el código '{1}'.", - "loc.messages.PSLIB_Required0": "Se requiere: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Error de formato de cadena.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "No se encuentra la clave de recurso de la cadena: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "Variable de tarea '{0}'" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson deleted file mode 100644 index dc2da05..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-FR/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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}'", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Échec de l'énumération des sous-répertoires pour le chemin : '{0}'", - "loc.messages.PSLIB_FileNotFound0": "Fichier introuvable : {0}.", - "loc.messages.PSLIB_Input0": "Entrée '{0}'", - "loc.messages.PSLIB_InvalidPattern0": "Modèle non valide : '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Le chemin feuille est introuvable : '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Échec de la normalisation/l'expansion du chemin. La longueur du chemin n'a pas été retournée par le sous-système Kernel32 pour : '{0}'", - "loc.messages.PSLIB_PathNotFound0": "Chemin introuvable : '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Le processus '{0}' s'est arrêté avec le code '{1}'.", - "loc.messages.PSLIB_Required0": "Obligatoire : {0}", - "loc.messages.PSLIB_StringFormatFailed": "Échec du format de la chaîne.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "Clé de la ressource de type chaîne introuvable : '{0}'", - "loc.messages.PSLIB_TaskVariable0": "Variable de tâche '{0}'" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson deleted file mode 100644 index 77bea53..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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}'", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "L'enumerazione delle sottodirectory per il percorso '{0}' non è riuscita", - "loc.messages.PSLIB_FileNotFound0": "File non trovato: '{0}'", - "loc.messages.PSLIB_Input0": "Input di '{0}'", - "loc.messages.PSLIB_InvalidPattern0": "Criterio non valido: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Percorso foglia non trovato: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "La normalizzazione o l'espansione del percorso non è riuscita. Il sottosistema Kernel32 non ha restituito la lunghezza del percorso per '{0}'", - "loc.messages.PSLIB_PathNotFound0": "Percorso non trovato: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Il processo '{0}' è stato terminato ed è stato restituito il codice '{1}'.", - "loc.messages.PSLIB_Required0": "Obbligatorio: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Errore nel formato della stringa.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "La chiave della risorsa stringa non è stata trovata: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "Variabile dell'attività '{0}'" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson deleted file mode 100644 index 9f2f9fe..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-JP/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "バージョン {0} 以降のエージェントが必要です。", - "loc.messages.PSLIB_ContainerPathNotFound0": "コンテナーのパスが見つかりません: '{0}'", - "loc.messages.PSLIB_EndpointAuth0": "'{0}' サービス エンドポイントの資格情報", - "loc.messages.PSLIB_EndpointUrl0": "'{0}' サービス エンドポイントの URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "パス '{0}' のサブディレクトリを列挙できませんでした", - "loc.messages.PSLIB_FileNotFound0": "ファイルが見つかりません: '{0}'", - "loc.messages.PSLIB_Input0": "'{0}' 入力", - "loc.messages.PSLIB_InvalidPattern0": "使用できないパターンです: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "リーフ パスが見つかりません: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "パスの正規化/展開に失敗しました。Kernel32 サブシステムからパス '{0}' の長さが返されませんでした", - "loc.messages.PSLIB_PathNotFound0": "パスが見つかりません: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "プロセス '{0}' がコード '{1}' で終了しました。", - "loc.messages.PSLIB_Required0": "必要: {0}", - "loc.messages.PSLIB_StringFormatFailed": "文字列のフォーマットに失敗しました。", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "文字列のリソース キーが見つかりません: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' タスク変数" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson deleted file mode 100644 index d809a2e..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "에이전트 버전 {0} 이상이 필요합니다.", - "loc.messages.PSLIB_ContainerPathNotFound0": "컨테이너 경로를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_EndpointAuth0": "'{0}' 서비스 엔드포인트 자격 증명", - "loc.messages.PSLIB_EndpointUrl0": "'{0}' 서비스 엔드포인트 URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "경로에 대해 하위 디렉터리를 열거하지 못함: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "{0} 파일을 찾을 수 없습니다.", - "loc.messages.PSLIB_Input0": "'{0}' 입력", - "loc.messages.PSLIB_InvalidPattern0": "잘못된 패턴: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "Leaf 경로를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "경로 정규화/확장에 실패했습니다. 다음에 대해 Kernel32 subsystem에서 경로 길이를 반환하지 않음: '{0}'", - "loc.messages.PSLIB_PathNotFound0": "경로를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "'{1}' 코드로 '{0}' 프로세스가 종료되었습니다.", - "loc.messages.PSLIB_Required0": "필수: {0}", - "loc.messages.PSLIB_StringFormatFailed": "문자열을 포맷하지 못했습니다.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "문자열 리소스 키를 찾을 수 없음: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' 작업 변수" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson deleted file mode 100644 index 40daae7..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "Требуется версия агента {0} или более поздняя.", - "loc.messages.PSLIB_ContainerPathNotFound0": "Путь к контейнеру не найден: \"{0}\".", - "loc.messages.PSLIB_EndpointAuth0": "Учетные данные конечной точки службы \"{0}\"", - "loc.messages.PSLIB_EndpointUrl0": "URL-адрес конечной точки службы \"{0}\"", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Сбой перечисления подкаталогов для пути: \"{0}\".", - "loc.messages.PSLIB_FileNotFound0": "Файл не найден: \"{0}\"", - "loc.messages.PSLIB_Input0": "Входные данные \"{0}\".", - "loc.messages.PSLIB_InvalidPattern0": "Недопустимый шаблон: \"{0}\".", - "loc.messages.PSLIB_LeafPathNotFound0": "Путь к конечному объекту не найден: \"{0}\".", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Сбой нормализации и расширения пути. Длина пути не была возвращена подсистемой Kernel32 для: \"{0}\".", - "loc.messages.PSLIB_PathNotFound0": "Путь не найден: \"{0}\".", - "loc.messages.PSLIB_Process0ExitedWithCode1": "Процесс \"{0}\" завершил работу с кодом \"{1}\".", - "loc.messages.PSLIB_Required0": "Требуется: {0}", - "loc.messages.PSLIB_StringFormatFailed": "Сбой формата строки.", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "Ключ ресурса строки не найден: \"{0}\".", - "loc.messages.PSLIB_TaskVariable0": "Переменная задачи \"{0}\"" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson deleted file mode 100644 index 49d824b..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "需要代理版本 {0} 或更高版本。", - "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路径:“{0}”", - "loc.messages.PSLIB_EndpointAuth0": "“{0}”服务终结点凭据", - "loc.messages.PSLIB_EndpointUrl0": "“{0}”服务终结点 URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "枚举路径的子目录失败:“{0}”", - "loc.messages.PSLIB_FileNotFound0": "找不到文件: {0}。", - "loc.messages.PSLIB_Input0": "“{0}”输入", - "loc.messages.PSLIB_InvalidPattern0": "无效的模式:“{0}”", - "loc.messages.PSLIB_LeafPathNotFound0": "找不到叶路径:“{0}”", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "路径规范化/扩展失败。路径长度不是由“{0}”的 Kernel32 子系统返回的", - "loc.messages.PSLIB_PathNotFound0": "找不到路径:“{0}”", - "loc.messages.PSLIB_Process0ExitedWithCode1": "过程“{0}”已退出,代码为“{1}”。", - "loc.messages.PSLIB_Required0": "必需: {0}", - "loc.messages.PSLIB_StringFormatFailed": "字符串格式无效。", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "找不到字符串资源关键字:“{0}”", - "loc.messages.PSLIB_TaskVariable0": "“{0}”任务变量" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson deleted file mode 100644 index 7cbf22e..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - "loc.messages.PSLIB_AgentVersion0Required": "需要代理程式版本 {0} 或更新的版本。", - "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路徑: '{0}'", - "loc.messages.PSLIB_EndpointAuth0": "'{0}' 服務端點認證", - "loc.messages.PSLIB_EndpointUrl0": "'{0}' 服務端點 URL", - "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "為路徑列舉子目錄失敗: '{0}'", - "loc.messages.PSLIB_FileNotFound0": "找不到檔案: '{0}'", - "loc.messages.PSLIB_Input0": "'{0}' 輸入", - "loc.messages.PSLIB_InvalidPattern0": "模式無效: '{0}'", - "loc.messages.PSLIB_LeafPathNotFound0": "找不到分葉路徑: '{0}'", - "loc.messages.PSLIB_PathLengthNotReturnedFor0": "路徑正規化/展開失敗。Kernel32 子系統未傳回 '{0}' 的路徑長度", - "loc.messages.PSLIB_PathNotFound0": "找不到路徑: '{0}'", - "loc.messages.PSLIB_Process0ExitedWithCode1": "處理序 '{0}' 以返回碼 '{1}' 結束。", - "loc.messages.PSLIB_Required0": "必要項: {0}", - "loc.messages.PSLIB_StringFormatFailed": "字串格式失敗。", - "loc.messages.PSLIB_StringResourceKeyNotFound0": "找不到字串資源索引鍵: '{0}'", - "loc.messages.PSLIB_TaskVariable0": "'{0}' 工作變數" -} \ No newline at end of file diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/ToolFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/ToolFunctions.ps1 deleted file mode 100644 index bb6f8d6..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/ToolFunctions.ps1 +++ /dev/null @@ -1,227 +0,0 @@ -<# -.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. - -.PARAMETER PassThru -True to return the path. -#> -function Assert-Path { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$LiteralPath, - [Microsoft.PowerShell.Commands.TestPathType]$PathType = [Microsoft.PowerShell.Commands.TestPathType]::Any, - [switch]$PassThru) - - if ($PathType -eq [Microsoft.PowerShell.Commands.TestPathType]::Any) { - Write-Verbose "Asserting path exists: '$LiteralPath'" - } - else { - Write-Verbose "Asserting $("$PathType".ToLowerInvariant()) path exists: '$LiteralPath'" - } - - if (Test-Path -LiteralPath $LiteralPath -PathType $PathType) { - if ($PassThru) { - return $LiteralPath - } - - return - } - - $resourceKey = switch ($PathType) { - ([Microsoft.PowerShell.Commands.TestPathType]::Container) { "PSLIB_ContainerPathNotFound0" ; break } - ([Microsoft.PowerShell.Commands.TestPathType]::Leaf) { "PSLIB_LeafPathNotFound0" ; break } - default { "PSLIB_PathNotFound0" } - } - - throw (Get-LocString -Key $resourceKey -ArgumentList $LiteralPath) -} - -<# -.SYNOPSIS -Executes an external program. - -.DESCRIPTION -Executes an external program and waits for the process to exit. - -After calling this command, the exit code of the process can be retrieved from the variable $LASTEXITCODE. - -.PARAMETER Encoding -This parameter not required for most scenarios. Indicates how to interpret the encoding from the external program. An example use case would be if an external program outputs UTF-16 XML and the output needs to be parsed. - -.PARAMETER RequireExitCodeZero -Indicates whether to write an error to the error pipeline if the exit code is not zero. -#> -function Invoke-Tool { - [CmdletBinding()] - param( - [ValidatePattern('^[^\r\n]*$')] - [Parameter(Mandatory = $true)] - [string]$FileName, - [ValidatePattern('^[^\r\n]*$')] - [Parameter()] - [string]$Arguments, - [string]$WorkingDirectory, - [System.Text.Encoding]$Encoding, - [switch]$RequireExitCodeZero, - [bool]$IgnoreHostException) - - Trace-EnteringInvocation $MyInvocation - $isPushed = $false - $originalEncoding = $null - try { - if ($Encoding) { - $originalEncoding = [System.Console]::OutputEncoding - [System.Console]::OutputEncoding = $Encoding - } - - if ($WorkingDirectory) { - Push-Location -LiteralPath $WorkingDirectory -ErrorAction Stop - $isPushed = $true - } - - $FileName = $FileName.Replace('"', '').Replace("'", "''") - Write-Host "##[command]""$FileName"" $Arguments" - try { - Invoke-Expression "& '$FileName' --% $Arguments" - } - catch [System.Management.Automation.Host.HostException] { - if ($IgnoreHostException -eq $False) { - throw - } - - Write-Host "##[warning]Host Exception was thrown by Invoke-Expression, suppress it due IgnoreHostException setting" - } - Write-Verbose "Exit code: $LASTEXITCODE" - if ($RequireExitCodeZero -and $LASTEXITCODE -ne 0) { - Write-Error (Get-LocString -Key PSLIB_Process0ExitedWithCode1 -ArgumentList ([System.IO.Path]::GetFileName($FileName)), $LASTEXITCODE) - } - } - finally { - if ($originalEncoding) { - [System.Console]::OutputEncoding = $originalEncoding - } - - if ($isPushed) { - Pop-Location - } - - Trace-LeavingInvocation $MyInvocation - } -} - -<# -.SYNOPSIS -Executes an external program as a child process. - -.DESCRIPTION -Executes an external program and waits for the process to exit. - -After calling this command, the exit code of the process can be retrieved from the variable $LASTEXITCODE or from the pipe. - -.PARAMETER FileName -File name (path) of the program to execute. - -.PARAMETER Arguments -Arguments to pass to the program. - -.PARAMETER StdOutPath -Path to a file to write the stdout of the process to. - -.PARAMETER StdErrPath -Path to a file to write the stderr of the process to. - -.PARAMETER RequireExitCodeZero -Indicates whether to write an error to the error pipeline if the exit code is not zero. - -.OUTPUTS -Exit code of the invoked process. Also available through the $LASTEXITCODE. - -.NOTES -To change output encoding, redirect stdout to file and then read the file with the desired encoding. -#> -function Invoke-Process { - [CmdletBinding()] - param( - [ValidatePattern('^[^\r\n]*$')] - [Parameter(Mandatory = $true)] - [string]$FileName, - [ValidatePattern('^[^\r\n]*$')] - [Parameter()] - [string]$Arguments, - [string]$WorkingDirectory, - [string]$StdOutPath, - [string]$StdErrPath, - [switch]$RequireExitCodeZero - ) - - Trace-EnteringInvocation $MyInvocation - try { - $FileName = $FileName.Replace('"', '').Replace("'", "''") - Write-Host "##[command]""$FileName"" $Arguments" - - $processOptions = @{ - FilePath = $FileName - NoNewWindow = $true - PassThru = $true - } - if ($Arguments) { - $processOptions.Add("ArgumentList", $Arguments) - } - if ($WorkingDirectory) { - $processOptions.Add("WorkingDirectory", $WorkingDirectory) - } - if ($StdOutPath) { - $processOptions.Add("RedirectStandardOutput", $StdOutPath) - } - if ($StdErrPath) { - $processOptions.Add("RedirectStandardError", $StdErrPath) - } - - # TODO: For some reason, -Wait is not working on agent. - # Agent starts executing the System usage metrics and hangs the step forever. - $proc = Start-Process @processOptions - - # https://stackoverflow.com/a/23797762 - $null = $($proc.Handle) - $proc.WaitForExit() - - $procExitCode = $proc.ExitCode - Write-Verbose "Exit code: $procExitCode" - - if ($RequireExitCodeZero -and $procExitCode -ne 0) { - Write-Error (Get-LocString -Key PSLIB_Process0ExitedWithCode1 -ArgumentList ([System.IO.Path]::GetFileName($FileName)), $procExitCode) - } - - $global:LASTEXITCODE = $procExitCode - - return $procExitCode - } - finally { - Trace-LeavingInvocation $MyInvocation - } -} diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/TraceFunctions.ps1 b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/TraceFunctions.ps1 deleted file mode 100644 index cdbd779..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/TraceFunctions.ps1 +++ /dev/null @@ -1,139 +0,0 @@ -<# -.SYNOPSIS -Writes verbose information about the invocation being entered. - -.DESCRIPTION -Used to trace verbose information when entering a function/script. Writes an entering message followed by a short description of the invocation. Additionally each bound parameter and unbound argument is also traced. - -.PARAMETER Parameter -Wildcard pattern to control which bound parameters are traced. -#> -function Trace-EnteringInvocation { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [System.Management.Automation.InvocationInfo]$InvocationInfo, - [string[]]$Parameter = '*') - - Write-Verbose "Entering $(Get-InvocationDescription $InvocationInfo)." - $OFS = ", " - if ($InvocationInfo.BoundParameters.Count -and $Parameter.Count) { - if ($Parameter.Count -eq 1 -and $Parameter[0] -eq '*') { - # Trace all parameters. - foreach ($key in $InvocationInfo.BoundParameters.Keys) { - Write-Verbose " $($key): '$($InvocationInfo.BoundParameters[$key])'" - } - } else { - # Trace matching parameters. - foreach ($key in $InvocationInfo.BoundParameters.Keys) { - foreach ($p in $Parameter) { - if ($key -like $p) { - Write-Verbose " $($key): '$($InvocationInfo.BoundParameters[$key])'" - break - } - } - } - } - } - - # Trace all unbound arguments. - if (@($InvocationInfo.UnboundArguments).Count) { - for ($i = 0 ; $i -lt $InvocationInfo.UnboundArguments.Count ; $i++) { - Write-Verbose " args[$i]: '$($InvocationInfo.UnboundArguments[$i])'" - } - } -} - -<# -.SYNOPSIS -Writes verbose information about the invocation being left. - -.DESCRIPTION -Used to trace verbose information when leaving a function/script. Writes a leaving message followed by a short description of the invocation. -#> -function Trace-LeavingInvocation { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [System.Management.Automation.InvocationInfo]$InvocationInfo) - - Write-Verbose "Leaving $(Get-InvocationDescription $InvocationInfo)." -} - -<# -.SYNOPSIS -Writes verbose information about paths. - -.DESCRIPTION -Writes verbose information about the paths. The paths are sorted and a the common root is written only once, followed by each relative path. - -.PARAMETER PassThru -Indicates whether to return the sorted paths. -#> -function Trace-Path { - [CmdletBinding()] - param( - [string[]]$Path, - [switch]$PassThru) - - if ($Path.Count -eq 0) { - Write-Verbose "No paths." - if ($PassThru) { - $Path - } - } elseif ($Path.Count -eq 1) { - Write-Verbose "Path: $($Path[0])" - if ($PassThru) { - $Path - } - } else { - # Find the greatest common root. - $sorted = $Path | Sort-Object - $firstPath = $sorted[0].ToCharArray() - $lastPath = $sorted[-1].ToCharArray() - $commonEndIndex = 0 - $j = if ($firstPath.Length -lt $lastPath.Length) { $firstPath.Length } else { $lastPath.Length } - for ($i = 0 ; $i -lt $j ; $i++) { - if ($firstPath[$i] -eq $lastPath[$i]) { - if ($firstPath[$i] -eq '\') { - $commonEndIndex = $i - } - } else { - break - } - } - - if ($commonEndIndex -eq 0) { - # No common root. - Write-Verbose "Paths:" - foreach ($p in $sorted) { - Write-Verbose " $p" - } - } else { - Write-Verbose "Paths: $($Path[0].Substring(0, $commonEndIndex + 1))" - foreach ($p in $sorted) { - Write-Verbose " $($p.Substring($commonEndIndex + 1))" - } - } - - if ($PassThru) { - $sorted - } - } -} - -######################################## -# Private functions. -######################################## -function Get-InvocationDescription { - [CmdletBinding()] - param([System.Management.Automation.InvocationInfo]$InvocationInfo) - - if ($InvocationInfo.MyCommand.Path) { - $InvocationInfo.MyCommand.Path - } elseif ($InvocationInfo.MyCommand.Name) { - $InvocationInfo.MyCommand.Name - } else { - $InvocationInfo.MyCommand.CommandType - } -} diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/VstsTaskSdk.dll b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/VstsTaskSdk.dll deleted file mode 100644 index 54938ab05180ddaf77cf067f2501a1f97ee7e5cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25408 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

8wuJ@>HyfDZ;=w9&`lhU;c$=Kxs zVyl3=oCzOR%(5JON~b1f_qN+!&dWl_U8}fsV#Ix3YxI$et=Qqtrbj{FWE$c^p03gw zvH`3R`Cpf{D%4sIAA!^?|@!O&A+cc|Ex;y|6?0F+wS9quHW9^=zrzn|8Li++DDT^mzVoUZBHf{%(A&` z+&F2uZRr5BJJx3?7i7uTq+cCqJ8j`#b-DS*ZPDce-qXtB9+EzMH(i&wvaNu;{++#% zcYtkc-G2VJ;sr0;Hp{x<5&6O@^OK@zp)En2H5kd8W?t#(tXU5Z@fTX(n%L1~H8J*xd0XA~tGY9FO!V{GAFRD#G3e@uMn47pstt`F{7#1- zWi49pHQ5e*sle1z<+k#Y0!F=g&RZiY9=|6+d&$Bv^Oy%)?#h}hI33WlP7MZ5srI`8 zDs#%WStH@>)5B>?n&RoQiFMb_FJ&u2u-$bQcf-@rv^-_J?e9(`T1x!=l656Xj>}^1^N?Y zEH15`x&O7D>6gB*9@02r$cOH;!{#ET_rTm5&V**TKN>0b;r!$&7 zOAU12dChrl?rV^B`RJpSS7)tK4q5!9fO%flyo3_*&h4K1ZC-4k&b8r6MwZ@JiaM)~ z+|?P{_g#wv|hI+^_}a5)~dOiu6HO_wO15Ta=@hEB<@82bDt@uz~NpDL_$1jV})P zncLo2a%j)IPX-%V{8`iIS(O(a!3qym6&}C}>;5B#_pd(&`QvX6gCiq|-yB-m+1Z-g zcv}0Id)j(=vrIALE_+?~S9_fqA;=PPxXd8|_?4dG}ryA~vq&3``QwMp3z+P|*{XsQBd)PR}pjq?*aeSuZZ! zRSve}Uh-LgC!#g_l=p1Kmv>pm+V?CrcJmz8xH#o_dB(OIMI$FLj3nmd>fAl(#7&wb zecG^fjm+4KckiE270=kN8Ii*_)Ob2PZJaG-S^BkBlM>gxoZV8x3k$bA^TtGY z@7tt9+x9W*H5SEK2-lwObj0@R961rZVGYTEzo-!}zCMc?y&%O1 zG7Ij;b)@+gJzBYZ`i5}x{cW+|S6l2U74)C1u;}1diVEIM-19nj@yVoFe~6!l z%wh{Uc5cN6D|N&D&*mS#H~7>BFH=p|<6jsSG3Qc?atAbZls%Lx^Y^#hYTvSsYPg&f zaAuI>jn3V3Xt^=l;%HyeiyFg8QdckPyCeg8Z=(>)u$VSSVt;j6wbe_ zw6|WRHhZ|_nuYUYyjeTqB4-qOB%91~SKi&KH{s{*;caH?8)~j|wf1CA91}&4Q(dOr ze1vf_*<|*suxPoIQ~R85&NbT^@0+1{t#L-gHs8oM&1 | - ForEach-Object { - if (`$_ -is [System.Management.Automation.ErrorRecord]) { - Write-Verbose `$_.Exception.Message - } else { - ,`$_ - } - } -} -"@ -. ([scriptblock]::Create($scriptText)) 2>&1 3>&1 4>&1 5>&1 | Out-Default - -# Create Invoke-VstsTaskScript in a special way so it is not bound to the module. -# Otherwise calling the task script block would run within the module context. -# -# An alternative way to solve the problem is to close the script block (i.e. closure). -# However, that introduces a different problem. Closed script blocks are created within -# a dynamic module. Each module gets it's own session state separate from the global -# session state. When running in a regular script context, Import-Module calls import -# the target module into the global session state. When running in a module context, -# Import-Module calls import the target module into the caller module's session state. -# -# The goal of a task may include executing ad-hoc scripts. Therefore, task scripts -# should run in regular script context. The end user specifying an ad-hoc script expects -# the module import rules to be consistent with the default behavior (i.e. imported -# into the global session state). -$null = New-Item -Force -Path "function:\global:Invoke-VstsTaskScript" -Value ([scriptblock]::Create(@' - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [scriptblock]$ScriptBlock) - - try { - $global:ErrorActionPreference = 'Stop' - - # Initialize the environment. - $vstsModule = Get-Module -Name VstsTaskSdk - Write-Verbose "$($vstsModule.Name) $($vstsModule.Version) commit $($vstsModule.PrivateData.PSData.CommitHash)" 4>&1 | Out-Default - & $vstsModule Initialize-Inputs 4>&1 | Out-Default - - # Remove the local variable before calling the user's script. - Remove-Variable -Name vstsModule - - # Call the user's script. - $ScriptBlock | - ForEach-Object { - # Remove the scriptblock variable before calling it. - Remove-Variable -Name ScriptBlock - & $_ 2>&1 3>&1 4>&1 5>&1 | Out-Default - } - } catch [VstsTaskSdk.TerminationException] { - # Special internal exception type to control the flow. Not currently intended - # for public usage and subject to change. - $global:__vstsNoOverrideVerbose = '' - Write-Verbose "Task script terminated." 4>&1 | Out-Default - } catch { - $global:__vstsNoOverrideVerbose = '' - Write-Verbose "Caught exception from task script." 4>&1 | Out-Default - $_ | Out-Default - Write-Host "##vso[task.complete result=Failed]" - } -'@)) diff --git a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/lib.json b/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/lib.json deleted file mode 100644 index 0cde160..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/ps_modules/VstsTaskSdk/lib.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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", - "PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Enumerating subdirectories failed for path: '{0}'", - "PSLIB_FileNotFound0": "File not found: '{0}'", - "PSLIB_Input0": "'{0}' input", - "PSLIB_InvalidPattern0": "Invalid pattern: '{0}'", - "PSLIB_LeafPathNotFound0": "Leaf path not found: '{0}'", - "PSLIB_PathLengthNotReturnedFor0": "Path normalization/expansion failed. The path length was not returned by the Kernel32 subsystem for: '{0}'", - "PSLIB_PathNotFound0": "Path not found: '{0}'", - "PSLIB_Process0ExitedWithCode1": "Process '{0}' exited with code '{1}'.", - "PSLIB_Required0": "Required: {0}", - "PSLIB_StringFormatFailed": "String format failed.", - "PSLIB_StringResourceKeyNotFound0": "String resource key not found: '{0}'", - "PSLIB_TaskVariable0": "'{0}' task variable" - } -} diff --git a/bc-tools-extension/Get-VSIXCompiler/task.json b/bc-tools-extension/Get-VSIXCompiler/task.json index 568669d..01b9381 100644 --- a/bc-tools-extension/Get-VSIXCompiler/task.json +++ b/bc-tools-extension/Get-VSIXCompiler/task.json @@ -36,9 +36,6 @@ }, "Node20_1": { "target": "function_Get-VSIXCompiler.js" - }, - "PowerShell3": { - "target": "wrapper_Get-VSIXCompiler.ps1" } } } diff --git a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 b/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 deleted file mode 100644 index 4aa05f5..0000000 --- a/bc-tools-extension/Get-VSIXCompiler/wrapper_Get-VSIXCompiler.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -function ConvertFrom-DevopsPath { - param([Parameter(Mandatory)][string]$Path) - if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - return [System.IO.Path]::GetFullPath($Path) - } else { - return $null - } -} - -. "./function_Get-VSIXCompiler.ps1" - -$localDownloadDirectory = Get-VstsInput -Name 'DownloadDirectory' -Require -$localCompilerVersion = Get-VstsInput -Name 'Version' -Require - -Write-Host "Getting AL Compiler:" -Write-Host (" {0,-30} = {1}" -f "DownloadDirectory", $localDownloadDirectory) -Write-Host (" {0,-30} = {1}" -f "Version", $localCompilerVersion) - -Write-Host "Normalizing directory reference: $localDownloadDirectory" -$localDownloadDirectory = ConvertFrom-DevopsPath $localDownloadDirectory -Write-Host "Normalized directory reference: $localDownloadDirectory" - -$vsixResult = Get-VSIXCompilerVersion -DownloadDirectory $localDownloadDirectory -Version $localCompilerVersion - -if (-not $vsixResult -or ` - [string]::IsNullOrWhiteSpace($vsixResult.Version) -or ` - [string]::IsNullOrWhiteSpace($vsixResult.ALEXEPath)) { - - Write-Error "Get-VSIXCompiler failed to return a valid Version and/or ALEXEPath." - exit 1 -} - -Write-Host "Variable assignments being set:" -Write-Host (" {0,-30} = {1}" -f "alVersion", $vsixResult.Version) -Write-Host "##vso[task.setvariable variable=alVersion;isOutput=true]$vsixResult.Version" -Write-Host (" {0,-30} = {1}" -f "alPath", $vsixResult.ALEXEPath) -Write-Host "##vso[task.setvariable variable=alPath;isOutput=true]$vsixResult.ALEXEPath" diff --git a/bc-tools-extension/_common/CommonTools.ps1 b/bc-tools-extension/_common/CommonTools.ps1 deleted file mode 100644 index b6aacad..0000000 --- a/bc-tools-extension/_common/CommonTools.ps1 +++ /dev/null @@ -1,29 +0,0 @@ - -function ConvertFrom-DevopsPath { - param([Parameter(Mandatory)][string]$Path) - if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - return [System.IO.Path]::GetFullPath($Path.Replace('/', '\')) - } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - return [System.IO.Path]::GetFullPath($Path) - } else { - return $null - } -} - -function Get-OSEnvironment { - if ($PSVersionTable.PSEdition -eq 'Core' -and $env:OS -like '*Windows*') { - return "win32" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - return "win" - } - elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { - return "linux" - } - else { - return "unknown" - } -} - From d2d0bb3a6be80b98029e3dd2f347d13c9717a7f1 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 13:24:35 -0600 Subject: [PATCH 105/130] Update RELEASE.md with quick hit before testing platform changes --- bc-tools-extension/RELEASE.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index b0faf24..919ff28 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -1,5 +1,6 @@ # Release Notes - BCBuildTasks Extension +- [Version: 0.1.8](#version-018) - [Version: 0.1.7](#version-017) - [Version: 0.1.6](#version-016) - [Version: 0.1.5](#version-015) @@ -26,6 +27,10 @@ * [Known Limitations](#known-limitations) * [Support](#support) +# Version: 0.1.8 + +- Addresses [#16](https://github.com/crazycga/bcdevopsextension/issues/16): Platform agnosticism; moving towards it. This update moves almost all Powershell code to use JavaScript, which is closer to native functionality and doesn't require the (soon to be deprecated?) VstsTaskSdk for Powershell. + # Version: 0.1.7 ## Fixes From 154d4621b107796bbef22e7c2811b7bc9008768e Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 15:26:55 -0600 Subject: [PATCH 106/130] Path normalization; improve logging and variable setting in Get-VSIXCompiler --- .../function_Build-ALPackage.js | 10 ++--- .../function_Get-BCDependencies.js | 6 +-- .../function_Get-VSIXCompiler.js | 39 ++++++++++++++++++- bc-tools-extension/_common/CommonTools.js | 8 +++- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js index 82a0e9e..bdaff32 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js @@ -2,14 +2,14 @@ const { spawn } = require('child_process'); const path = require('path'); const os = require('os'); const fs = require('fs'); -const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { logger, normalizePath } = require(path.join(__dirname, '_common', 'CommonTools.js')); // collect variables from input const entireAppName = process.env.INPUT_ENTIREAPPNAME; -const baseProjectDirectory = process.env.INPUT_PROJECTPATH; -const packagesDirectory = process.env.INPUT_PACKAGECACHEPATH; -const outputDirectory = process.env.INPUT_OUTAPPFOLDER; -const alcPath = process.env.INPUT_ALEXEPATHFOLDER; +const baseProjectDirectory = normalizePath(process.env.INPUT_PROJECTPATH); +const packagesDirectory = normalizePath(process.env.INPUT_PACKAGECACHEPATH); +const outputDirectory = normalizePath(process.env.INPUT_OUTAPPFOLDER); +const alcPath = normalizePath(process.env.INPUT_ALEXEPATHFOLDER); logger.info('Calling Build-ALPackage with the following parameters:'); logger.info('EntireAppName'.padStart(2).padEnd(30) + entireAppName); diff --git a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js index 569ee17..fb2ae40 100644 --- a/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js +++ b/bc-tools-extension/Get-BCDependencies/function_Get-BCDependencies.js @@ -3,7 +3,7 @@ const path = require('path'); const os = require('os'); const fs = require('fs'); const { PassThrough } = require('stream'); -const { usesUndici, logger, parseBool, getToken } = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { usesUndici, logger, parseBool, getToken, normalizePath } = require(path.join(__dirname, '_common', 'CommonTools.js')); const fetch = usesUndici(); (async () => { @@ -12,8 +12,8 @@ const fetch = usesUndici(); const environmentName = process.env.INPUT_ENVIRONMENTNAME || 'sandbox'; const clientId = process.env.INPUT_CLIENTID; const clientSecret = process.env.INPUT_CLIENTSECRET; - let pathToAppJson = process.env.INPUT_PATHTOAPPJSON; - const pathToPackagesDirectory = process.env.INPUT_PATHTOPACKAGESDIRECTORY; + let pathToAppJson = normalizePath(process.env.INPUT_PATHTOAPPJSON); + const pathToPackagesDirectory = normalizePath(process.env.INPUT_PATHTOPACKAGESDIRECTORY); const testLoginOnly = process.env.INPUT_TESTLOGINONLY; const skipDefaultDependencies = process.env.INPUT_SKIPDEFAULTDEPENDENCIES; diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js index 1f1cea2..8e248a3 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js @@ -2,15 +2,36 @@ const { spawn } = require('child_process'); const path = require('path'); const os = require('os'); const fs = require('fs'); +const fsp = require('fs/promises'); const { PassThrough } = require('stream'); -const { usesUndici, logger, parseBool, getToken } = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { usesUndici, logger, parseBool, getToken, normalizePath } = require(path.join(__dirname, '_common', 'CommonTools.js')); const fetch = usesUndici(); const crypto = require('crypto'); const { pipeline } = require('stream/promises'); +// helper function to find the compiler (can allow an array in targetname) +async function findCompiler(dir, targetname) { + const entries = await fsp.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name.toLowerCase()); + + const match = Array.isArray(targetname) ? targetname.some(t => fullPath.includes(t.toLowerCase())) : fullPath.includes(targetname.toLowerCase()); + + if (entry.isDirectory()) { + const result = await findCompiler(fullPath, targetname); + if (result) return result; + } else if (match) { + return fullPath; + } + } + return null; +} + +// main function of script (async () => { // collect variables from input - const downloadDirectory = process.env.INPUT_DOWNLOADDIRECTORY; + const downloadDirectory = normalizePath(process.env.INPUT_DOWNLOADDIRECTORY); const downloadVersion = process.env.INPUT_VERSION; logger.info('Calling Get-VSIXCompiler with the following parameters:'); @@ -176,4 +197,18 @@ const { pipeline } = require('stream/promises'); logger.info(`Extracting ${downloadFilename} to ${extractTo}`); await fs.createReadStream(downloadFilename).pipe(unzipper.Extract( {path: extractTo })).promise(); logger.info(`Extracted ${downloadFilename} to ${extractTo}`); + + // find alc / alc.exe and echo results + + const expectedCompilerName = isWindows ? 'alc.exe' : 'alc'; + logger.debug(`Searching for compiler ${expectedCompilerName}`); + let actualALEXE = await findCompiler(extractTo, expectedCompilerName); + + if (actualALEXE) { + logger.info(`Found compiler '${expectedCompilerName}' at '${actualALEXE}`); + logger.info(`##vso[task.setvariable variable=alVersion;isOutput=true]${version}`); + logger.info(`Set pipeline variable 'alVersion' to ${version}`); + logger.info(`##vso[task.setvariable variable=alPath;isOutput=true]${actualALEXE}`); + logger.info(`Set pipeline variable 'alPath' to ${actualALEXE}`); + } })(); \ No newline at end of file diff --git a/bc-tools-extension/_common/CommonTools.js b/bc-tools-extension/_common/CommonTools.js index 03abea4..0b0c1e7 100644 --- a/bc-tools-extension/_common/CommonTools.js +++ b/bc-tools-extension/_common/CommonTools.js @@ -35,6 +35,11 @@ if (parseBool(testMode)) { logger.info(`Invocation received with TestMode: ${testMode}`); } +// sanitize file paths and names, etc. +function normalizePath(p) { + return p.replace(/\\/g, '/'); +} + // this module uses undici for fetching specifically because the call to Microsoft.NAV.upload will return malformed and node-fetch can't parse it let fetch = null; @@ -590,5 +595,6 @@ module.exports = { waitForResponse, parseBool, logger, - usesUndici + usesUndici, + normalizePath } \ No newline at end of file From c1c42d3fa620accade36e3ae1a487bfe7498ceff Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 15:52:11 -0600 Subject: [PATCH 107/130] Fix the directory walking routine --- .../function_Get-VSIXCompiler.js | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js index 8e248a3..0b7cc6d 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js @@ -9,25 +9,41 @@ const fetch = usesUndici(); const crypto = require('crypto'); const { pipeline } = require('stream/promises'); -// helper function to find the compiler (can allow an array in targetname) +// Can accept either a string or array in `targetname` async function findCompiler(dir, targetname) { - const entries = await fsp.readdir(dir, { withFileTypes: true }); + let entries; + try { + entries = await fsp.readdir(dir, { withFileTypes: true }); + logger.debug(`Searching directory: ${dir}`); + } catch (err) { + if (err.code === 'ENOENT') { + // Skip if directory doesn't exist + logger.debug(`Skipping missing directory: ${dir}`); + return null; + } else { + // Unexpected error — surface it + throw err; + } + } for (const entry of entries) { - const fullPath = path.join(dir, entry.name.toLowerCase()); + const fullPath = path.join(dir, entry.name); - const match = Array.isArray(targetname) ? targetname.some(t => fullPath.includes(t.toLowerCase())) : fullPath.includes(targetname.toLowerCase()); - if (entry.isDirectory()) { const result = await findCompiler(fullPath, targetname); if (result) return result; - } else if (match) { - return fullPath; + } else { + const name = entry.name.toLowerCase(); + const matches = Array.isArray(targetname) ? targetname.some(t => name.includes(t.toLowerCase())) : name.includes(targetname.toLowerCase()); + if (matches) { + return fullPath; + } } } return null; } + // main function of script (async () => { // collect variables from input From 58127130e4f1cd00e65b687143e2784da9863c80 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 16:02:05 -0600 Subject: [PATCH 108/130] Fix linux alc call --- .../Build-ALPackage/function_Build-ALPackage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js index bdaff32..c987ae3 100644 --- a/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js +++ b/bc-tools-extension/Build-ALPackage/function_Build-ALPackage.js @@ -90,9 +90,9 @@ const args = isWindows `/packageCachePath:${packagesDirectory}` ] : [ - '--project', baseProjectDirectory, - '--out', outputFile, - '--packagecachepath', packagesDirectory + `/project:${baseProjectDirectory}`, + `/out:${outputFile}`, + `/packageCachePath:${packagesDirectory}` ]; // attempt execution of ALC From 8b9b4b0a515700e8ad611e134df91a214b152989 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 16:11:16 -0600 Subject: [PATCH 109/130] Fix path normalization in deploy --- .../function_Publish-BCModuleToTenant.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js index b18a276..1ce8eaa 100644 --- a/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js +++ b/bc-tools-extension/Publish-BCModuleToTenant/function_Publish-BCModuleToTenant.js @@ -1,13 +1,13 @@ const path = require('path'); const commonTools = require(path.join(__dirname, '_common', 'CommonTools.js')); -const { logger } = require(path.join(__dirname, '_common', 'CommonTools.js')); +const { logger, normalizePath } = require(path.join(__dirname, '_common', 'CommonTools.js')); const tenantId = process.env.INPUT_TENANTID; const clientId = process.env.INPUT_CLIENTID; const clientSecret = process.env.INPUT_CLIENTSECRET; const environmentName = process.env.INPUT_ENVIRONMENTNAME; const companyId = process.env.INPUT_COMPANYID; -const filePath = process.env.INPUT_APPFILEPATH; +const filePath = normalizePath(process.env.INPUT_APPFILEPATH); const skipPolling = commonTools.parseBool(process.env.INPUT_SKIPPOLLING); const pollingFrequency = parseInt(process.env.INPUT_POLLINGFREQUENCY); const maxTimeout = parseInt(process.env.INPUT_MAXPOLLINGTIMEOUT); From 775e92e15cf64dd267b2fc197e207a6547bd76a6 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 10 Jun 2025 16:37:59 -0600 Subject: [PATCH 110/130] Final documentation update; RC 0.1.8 --- bc-tools-extension/README.md | 16 +++++++++++----- bc-tools-extension/RELEASE.md | 2 +- bc-tools-extension/overview.md | 4 +--- bc-tools-extension/test.js | 11 ----------- 4 files changed, 13 insertions(+), 20 deletions(-) delete mode 100644 bc-tools-extension/test.js diff --git a/bc-tools-extension/README.md b/bc-tools-extension/README.md index 621b5a0..3e44b1b 100644 --- a/bc-tools-extension/README.md +++ b/bc-tools-extension/README.md @@ -22,18 +22,24 @@ ## Overview -**WINDOWS AGENTS ONLY** +**2025-06-10 Version 0.1.8: NO LONGER WINDOWS ONLY!** -This Azure DevOps extension provides build pipeline tasks for Microsoft Dynamics 365 Business Central AL projects. It enables full pipeline-based compilation, dependency acquisition, and VSIX compiler management using custom PowerShell-backed tasks. +*Proud to announce that this extension is NO LONGER Windows agent dependent!!!* -**This extension is only usable on Windows-based agents** +This Azure DevOps extension provides build pipeline tasks for Microsoft Dynamics 365 Business Central AL projects. It enables full pipeline-based compilation, dependency acquisition, and VSIX compiler management using custom tasks. + +**This extension is usable on both Ubuntu and Windows based agents** [![main-build](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml/badge.svg?branch=main)](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml) -[![main-build](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml/badge.svg?branch=dev_trunk)](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml) +[![dev-trunk](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml/badge.svg?branch=dev_trunk)](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml) ## Features +* **Platform Independent** + + * Operates on both Ubuntu and Windows agents + * ✅ **Get AL Compiler** * Downloads the latest version of the Business Central AL compiler from the Visual Studio Marketplace @@ -161,7 +167,7 @@ You want to provide this user with the out-of-the-box permission set `EXTEN. MGT **Notes:** -If not using the `alVersion` variable from above, the system places the expanded archive in the `$(DownloadDirectory)\expanded\extension\bin` folder. (Technically it then goes one level lower, to the `win32` folder.) +If not using the `alVersion` variable from above, the system places the expanded archive in the `$(DownloadDirectory)\expanded\extension\bin` folder. (Technically it then goes one level lower, to the `win32` folder or the `linux` folder.) ### 2. Get AL Dependencies (`EGGetALDependencies`) diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index 919ff28..dcaf761 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -29,7 +29,7 @@ # Version: 0.1.8 -- Addresses [#16](https://github.com/crazycga/bcdevopsextension/issues/16): Platform agnosticism; moving towards it. This update moves almost all Powershell code to use JavaScript, which is closer to native functionality and doesn't require the (soon to be deprecated?) VstsTaskSdk for Powershell. +- Addresses [#16](https://github.com/crazycga/bcdevopsextension/issues/16): Platform agnosticism; moving towards it. This update moves almost all Powershell code to use JavaScript, which is closer to native functionality and doesn't require the (soon to be deprecated, maybe?) VstsTaskSdk for Powershell. # Version: 0.1.7 diff --git a/bc-tools-extension/overview.md b/bc-tools-extension/overview.md index 86b93c0..dff588b 100644 --- a/bc-tools-extension/overview.md +++ b/bc-tools-extension/overview.md @@ -2,11 +2,9 @@ ## Overview -**WINDOWS AGENTS ONLY** - This Azure DevOps extension provides build pipeline tasks for Microsoft Dynamics 365 Business Central AL projects. It enables full pipeline-based compilation, dependency acquisition, and VSIX compiler management using custom PowerShell-backed tasks. -**This extension is only usable on Windows-based agents** +**This extension is usable on both Ubuntu and Windows based agents** ------------------------------------- diff --git a/bc-tools-extension/test.js b/bc-tools-extension/test.js deleted file mode 100644 index 24be0c8..0000000 --- a/bc-tools-extension/test.js +++ /dev/null @@ -1,11 +0,0 @@ - const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms * 1000)); - const startTimeStamp = Date.now(); - let currentTimeStamp = Date.now(); - - console.log(`Waiting an initial 10 seconds before polling...`); - await sleep(10); - - currentTimeStamp = Date.now(); - let elapsed = (currentTimeStamp - startTimeStamp) / 1000; - console.log(elapsed, 'seconds passed'); - From b847655f821c8affa5f22ed1c63e5bda2fe22f37 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Sat, 14 Jun 2025 13:24:21 -0600 Subject: [PATCH 111/130] Candidate to fix incorrect compiler issue Fixes #31 --- _tasks/environments.json | 2 +- .../function_Get-VSIXCompiler.js | 74 +++++++++++-------- bc-tools-extension/RELEASE.md | 4 + 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/_tasks/environments.json b/_tasks/environments.json index 39e8fc5..84b8b2a 100644 --- a/_tasks/environments.json +++ b/_tasks/environments.json @@ -2,7 +2,7 @@ "version": { "major": 0, "minor": 1, - "patch": 8, + "patch": 9, "build": 0 }, "dev": { diff --git a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js index 0b7cc6d..b4e201e 100644 --- a/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js +++ b/bc-tools-extension/Get-VSIXCompiler/function_Get-VSIXCompiler.js @@ -10,37 +10,49 @@ const crypto = require('crypto'); const { pipeline } = require('stream/promises'); // Can accept either a string or array in `targetname` -async function findCompiler(dir, targetname) { - let entries; - try { - entries = await fsp.readdir(dir, { withFileTypes: true }); - logger.debug(`Searching directory: ${dir}`); - } catch (err) { - if (err.code === 'ENOENT') { - // Skip if directory doesn't exist - logger.debug(`Skipping missing directory: ${dir}`); - return null; - } else { - // Unexpected error — surface it - throw err; - } - } +async function findCompiler(baseDir, isWindows) { + const subfolder = isWindows ? 'win32' : 'linux'; + const targetname = isWindows ? 'alc.exe' : 'alc'; + const expectedRoot = path.join(baseDir, 'extension', 'bin', subfolder); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); + async function walk(dir) { + let entries; + try { + entries = await fsp.readdir(dir, { withFileTypes: true }); + logger.debug(`Searching directory: ${dir}`); + } catch (err) { + if (err.code === 'ENOENT') { + // Skip if directory doesn't exist + logger.debug(`Skipping missing directory: ${dir}`); + return null; + } else { + throw err; + } + } - if (entry.isDirectory()) { - const result = await findCompiler(fullPath, targetname); - if (result) return result; - } else { - const name = entry.name.toLowerCase(); - const matches = Array.isArray(targetname) ? targetname.some(t => name.includes(t.toLowerCase())) : name.includes(targetname.toLowerCase()); - if (matches) { - return fullPath; + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + const result = await walk(fullPath); + if (result) return result; + } else { + const name = entry.name.toLowerCase(); + const matches = Array.isArray(targetname) + ? targetname.some(t => name === t.toLowerCase()) // ✅ correct + : name === targetname.toLowerCase(); + if (matches) { + return fullPath; + } } } + + return null; } - return null; + + const result = await walk(expectedRoot); + if (!result) throw new Error(`Could not find compiler ${targetname} in '${expectedRoot}'`); + return result; } @@ -105,7 +117,7 @@ async function findCompiler(dir, targetname) { logger.debug(`JSON body: ${JSON.stringify(jsonRawPrototype)}`); let versionResult; - + try { const versionResponse = await fetch(apiUrl, { method: 'POST', @@ -166,7 +178,7 @@ async function findCompiler(dir, targetname) { try { const downloadResponse = await fetch(downloadUrl); - if (!downloadResponse.ok) { + if (!downloadResponse.ok) { logger.error(`Something went wrong downloading the compiler; got response code ${downloadResponse.status}: ${downloadResponse.statusText}`); process.exit(1); } @@ -211,14 +223,14 @@ async function findCompiler(dir, targetname) { const extractTo = path.join(downloadDirectory, 'expanded'); logger.info(`Extracting ${downloadFilename} to ${extractTo}`); - await fs.createReadStream(downloadFilename).pipe(unzipper.Extract( {path: extractTo })).promise(); + await fs.createReadStream(downloadFilename).pipe(unzipper.Extract({ path: extractTo })).promise(); logger.info(`Extracted ${downloadFilename} to ${extractTo}`); // find alc / alc.exe and echo results - + const expectedCompilerName = isWindows ? 'alc.exe' : 'alc'; logger.debug(`Searching for compiler ${expectedCompilerName}`); - let actualALEXE = await findCompiler(extractTo, expectedCompilerName); + let actualALEXE = await findCompiler(extractTo, isWindows); if (actualALEXE) { logger.info(`Found compiler '${expectedCompilerName}' at '${actualALEXE}`); diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index dcaf761..9939ba9 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -1,5 +1,6 @@ # Release Notes - BCBuildTasks Extension +- [Version: 0.1.9](#version-019) - [Version: 0.1.8](#version-018) - [Version: 0.1.7](#version-017) - [Version: 0.1.6](#version-016) @@ -27,6 +28,9 @@ * [Known Limitations](#known-limitations) * [Support](#support) +# Version: 0.1.9 +- Addresses [#31](https://github.com/crazycga/bcdevopsextension/issues/31): ```EGGetALCompiler``` returning incorrect value. This update addresses this particular bug, having refactored the search / walk algorithm to find the compiler. + # Version: 0.1.8 - Addresses [#16](https://github.com/crazycga/bcdevopsextension/issues/16): Platform agnosticism; moving towards it. This update moves almost all Powershell code to use JavaScript, which is closer to native functionality and doesn't require the (soon to be deprecated, maybe?) VstsTaskSdk for Powershell. From ea55716a44f62df98926ed7ce02e59df8e906c12 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 13:54:51 -0600 Subject: [PATCH 112/130] Introduce environmental enumeration for bits and pieces --- _tasks/_build.ps1 | 3 +- _tasks/environments.json | 8 + .../function_Enumerate-Environment.js | 190 ++++++++++++++++++ .../Enumerate-Environment/task.json | 41 ++++ bc-tools-extension/README.md | 6 +- 5 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js create mode 100644 bc-tools-extension/Enumerate-Environment/task.json diff --git a/_tasks/_build.ps1 b/_tasks/_build.ps1 index 84ca8be..5059f41 100644 --- a/_tasks/_build.ps1 +++ b/_tasks/_build.ps1 @@ -16,7 +16,8 @@ $paths = @( "./Publish-BCModuleToTenant", "./Build-ALPackage", "./Get-BCDependencies", - "./Get-VSIXCompiler" + "./Get-VSIXCompiler", + "./Enumerate-Environment" ) foreach($path in $paths) { diff --git a/_tasks/environments.json b/_tasks/environments.json index 84b8b2a..585a4a0 100644 --- a/_tasks/environments.json +++ b/_tasks/environments.json @@ -38,6 +38,10 @@ "EGDeployBCModule": { "location": "bc-tools-extension/Publish-BCModuleToTenant/task.json", "taskGuid": "def7c0a0-0d00-4f62-ae3f-7f084561e721" + }, + "EGEnumerateEnvironment": { + "location": "bc-tools-extension/Enumerate-Environment/task.json", + "taskGuid": "16821c4a-f2e7-4148-a686-2ec76cab618c" } } }, @@ -74,6 +78,10 @@ "EGDeployBCModule": { "location": "bc-tools-extension/Publish-BCModuleToTenant/task.json", "taskGuid": "7315a985-6da9-4b4a-bae9-56b04fc492fd" + }, + "EGEnumerateEnvironment": { + "location": "bc-tools-extension/Enumerate-Environment/task.json", + "taskGuid": "206d9815-c3dd-459f-b3c1-41e8b18dddab" } } } diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js new file mode 100644 index 0000000..ca8d968 --- /dev/null +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -0,0 +1,190 @@ +const { execSync } = require('child_process'); +const path = require('path'); +const os = require('os'); +const fs = require('fs'); +const { PassThrough } = require('stream'); +const { logger, parseBool, getToken, normalizePath } = require(path.join(__dirname, '_common', 'CommonTools.js')); + +let produceFile = parseBool(process.env.INPUT_PRODUCEFILE); +const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; + +// this routine is intended to provide information about the agent on which it is running +// +// 1. platform +// 2. whoami +// 3. current working directory +// 4. Powershell version(s) +// 5. BCContainerHelper existence / version +// 6. Docker existence / version +// 7. Docker image list + +(async () => { + let outputFilenameAndPath; + if (produceFile) { + if (inputFilenameAndPath && inputFilenameAndPath != '') { + outputFilenameAndPath = normalizePath(inputFilenameAndPath); + let pathInfo = path.parse(outputFilenameAndPath); + + let outputPath = pathInfo.dir; + let outputName = pathInfo.base; + + if (!outputName || !outputPath) { + logger.warn(`Requested a file output, but cannot parse the file name and path '${outputFilenameAndPath}'`); + produceFile = false; + logger.info(`Setting ProduceFile to ${produceFile}`); + } + } else { + logger.warn(`Requested a file output, but no file name and path was supplied in 'FilenameAndPath'`); + produceFile = false; + logger.info(`Setting ProduceFile to ${produceFile}`); + } + } + + logger.info('Invoking EGEnumerateEnvironment with the following parameters:'); + logger.info('ProduceFile:'.padStart(2).padEnd(30) + `${produceFile}`); + logger.info(''); + + // 0. setup + const logColWidth = 30; + + // 1. platform + logger.info('[platform]:'.padEnd(logColWidth) + `${os.platform()}`); + + // 2. whoami + let textOut; + try { + let whoami = execSync('whoami', { encoding: 'utf8'}); + textOut = whoami.toString().trim(); + if (textOut.length > 0) { + logger.info('[whoami]: '.padEnd(logColWidth) + `${textOut}`); + } else { + logger.info('[whoami]:'.padEnd(logColWidth) + 'Apparently a ghost; nothing returned'); + } + } catch (err) { + logger.error(`[whoami]: Encountered an error while executing a 'whoami'`); + logger.error(`[whoami]: Error: ${err}`); + } + + // 3. current working directory + logger.info('[current working directory]:'.padEnd(logColWidth) + `${process.cwd()}`); + + // 4. Powershell version(s) + let psVersion; + let pwshVersion; + try { + psVersion = execSync( + `powershell -NoProfile -Command "$v = $PSVersionTable.PSVersion; Write-Output ($v.Major.ToString() + '.' + $v.Minor + '.' + $v.Build + '.' + $v.Revision)"`, + { encoding: 'utf8' } + ).trim(); + logger.info('[powershell version]:'.padEnd(logColWidth) + `${psVersion}`); + } catch (err) { + logger.error(`[powershell version]: Encountered an error while executing a 'powerhsell version'`); + logger.error(`[powershell version]: Error: ${err}`); + } + + try { + pwshVersion = execSync( + `pwsh -NoProfile -Command "$v = $PSVersionTable.PSVersion; Write-Output ($v.Major.ToString() + '.' + $v.Minor + '.' + $v.Patch)"`, + { encoding: 'utf8' } + ).trim(); + logger.info('[pwsh version]:'.padEnd(logColWidth) + `${pwshVersion}`); + } catch (err) { + logger.error(`[pwsh version]: Encountered an error while executing a 'pwsh version'`); + logger.error(`[pwsh version]: Error: ${err}`); + } + + // 5. BCContainerHelper existence / version + let result; + let BCContainerHelperPresent = false; + try { + const psCommand = `$modulePath = Get-Module -ListAvailable BCContainerHelper | Select-Object -First 1 -ExpandProperty Path; if ($modulePath) { $psd1 = $modulePath -replace '\\[^\\\\]+$', '.psd1'; if (Test-Path $psd1) { $lines = Get-Content $psd1 -Raw; if ($lines -match 'ModuleVersion\\s*=\\s*[\\"\\'']?([0-9\\.]+)[\\"\\'']?') { Write-Output $matches[1]; } else { Write-Output '[version not found]'; } } else { Write-Output '[not installed]'; } } else { Write-Output '[not installed]'; }`; + + result = execSync(`powershell.exe -NoProfile -Command "${psCommand.replace(/\n/g, ' ').replace(/"/g, '\\"')}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + if (result === "") { result = '[not installed]'} + if (result && result != "") { + logger.info('[BCContainerHelper version]:'.padEnd(logColWidth) + `${result}`); + BCContainerHelperPresent = true; + } + BCContainerHelperPresent = true; + } catch (err) { + logger.error(`[BCContainerHelper]: Failed to query module: ${err.message}`); + logger.info(err); + } + + // 6. Docker existence / version + let DockerPresent = false; + let DockerVersionResult; + try { + DockerResult = execSync('docker version --format "{{.Client.Version}}"', { stdio: ['pipe', 'pipe', 'pipe'] }); + if (DockerResult === "") { DockerVersionResult = '[not installed]'} + else { DockerVersionResult = DockerResult.toString().trim(); } + if (DockerVersionResult && DockerVersionResult != "") { + logger.info('[dockerversion]:'.padEnd(logColWidth) + `${DockerVersionResult}`); + DockerPresent = true; + } + } catch (err) { + const msg = err.message || ''; + const stderr = err.stderr?.toString() || ''; + + const combined = `${msg}\n${stderr}`; + const normalized = combined.toLowerCase(); + if ( + normalized.includes("'docker' is not recognized") || // Windows case + normalized.includes("command not found") || // Linux case + normalized.includes("no such file or directory") // fallback + ) { + DockerVersionResult = '[not installed]'; + if (DockerVersionResult && DockerVersionResult != "") { + logger.info('[dockerversion]:'.padEnd(logColWidth) + `${DockerVersionResult}`); + } + } else { + logger.error(`[dockerversion]: Unexpected error: ${combined}`); + } + } + + // 7. Docker image list + let DockerPsObject; + if (DockerPresent) { + try { + const psResult = execSync('docker ps -a --no-trunc --format "{{json .}}"', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); + const lines = psResult.trim().split('\n'); + DockerPsObject = lines.map(line => JSON.parse(line)); + + logger.info('[dockerimage]:'.padEnd(logColWidth) + '**Name**'.padEnd(logColWidth) + '**Status**'); + DockerPsObject.forEach((image, idx) => { + if (image && image.name != "") { + logger.info('[dockerimage]:'.padEnd(logColWidth) + `${image.Names}`.padEnd(logColWidth) + `${image.Status}`); + } + }); + } catch (err) { + const msg = err.message || ''; + const stderr = err.stderr?.toString() || ''; + + const combined = `${msg}\n${stderr}`; + logger.error(`[dockerimage]: Unexpected error: ${combined}`); + } + } else { + logger.info('[dockerimage]:'.padEnd(logColWidth) + '[not installed]'); + } + + // Deal with the file if requested (note it has already been parsed at the top of this routine) + if (produceFile) { + + let candidateFile = { + platform: os.platform(), + whoami: textOut, + workingDirectory: process.cwd(), + powershellVersion: psVersion, + pscoreVersion: pwshVersion, + bcContainerVersion: result, + dockerVersion: DockerVersionResult, + dockerImages: DockerPsObject.filter(img => img && img.Names).map(img => ({ name: img.Names, status: img.Status })) + } + + let candidateFileString = JSON.stringify(candidateFile); + fs.writeFileSync(outputFilenameAndPath, candidateFileString); + + logger.info(''); + logger.info(`Produced file at: ${outputFilenameAndPath}`); + } +})(); \ No newline at end of file diff --git a/bc-tools-extension/Enumerate-Environment/task.json b/bc-tools-extension/Enumerate-Environment/task.json new file mode 100644 index 0000000..ca8b4ef --- /dev/null +++ b/bc-tools-extension/Enumerate-Environment/task.json @@ -0,0 +1,41 @@ +{ + "id": "0d4e6693-bdcb-47c0-a373-67a34549da07", + "name": "EGEnumerateEnvironment", + "friendlyName": "Enumerate compiling environment", + "description": "Provides information about the pipeline environment in the context of the agent.", + "helpMarkDown": "Please open a GitHub issue at https://github.com/crazycga/bcdevopsextension/issues for queries or support.", + "category": "Build", + "author": "Evergrowth Consulting", + "version": { + "Major": 0, + "Minor": 1, + "Patch": 5 + }, + "instanceNameFormat": "Enumerate compiling environment", + "inputs": [ + { + "name": "ProduceFile", + "type": "boolean", + "label": "Produce file", + "defaultValue": false, + "required": false, + "helpMarkDown": "Specifies whether or not to produce an output file as one of the artifacts." + }, + { + "name": "FilePathAndName", + "type": "string", + "label": "Output file path and name", + "defaultValue": "$(Build.ArtifactStagingDirectory)/environment.$(System.StageName).$(Agent.JobName).$(Build.BuildId).json", + "required": false, + "helpMarkDown": "The output path and name of the output file if specified; default '$(Build.ArtifactStagingDirectory)/environment.$(System.StageName).$(Agent.JobName).$(Build.BuildId).json'" + } + ], + "execution": { + "Node16": { + "target": "function_Enumerate-Environment.js" + }, + "Node20_1": { + "target": "function_Enumerate-Environment.js" + } + } +} diff --git a/bc-tools-extension/README.md b/bc-tools-extension/README.md index 3e44b1b..341dffc 100644 --- a/bc-tools-extension/README.md +++ b/bc-tools-extension/README.md @@ -32,7 +32,11 @@ This Azure DevOps extension provides build pipeline tasks for Microsoft Dynamics [![main-build](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml/badge.svg?branch=main)](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml) -[![dev-trunk](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml/badge.svg?branch=dev_trunk)](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml) +[![dev-trunk](https://img.shields.io/github/actions/workflow/status/crazycga/bcdevopsextension/mainbuild.yml?branch=dev_trunk&label=development)](https://github.com/crazycga/bcdevopsextension/actions/workflows/mainbuild.yml) + +![GitHub Release](https://img.shields.io/github/v/release/crazycga/bcdevopsextension) + +![Visual Studio Marketplace Installs - Azure DevOps Extension](https://img.shields.io/visual-studio-marketplace/azure-devops/installs/total/Evergrowth.eg-bc-build-tasks) ## Features From dd7114c25ab32a9e8d546260c3c574f23da08e52 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 14:08:56 -0600 Subject: [PATCH 113/130] Update vss extension metadata --- bc-tools-extension/vss-extension.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bc-tools-extension/vss-extension.json b/bc-tools-extension/vss-extension.json index a9d7afb..cc572d2 100644 --- a/bc-tools-extension/vss-extension.json +++ b/bc-tools-extension/vss-extension.json @@ -85,6 +85,9 @@ }, { "path": "_common" + }, + { + "path": "Enumerate-Environment" } ], "contributions": [ @@ -147,6 +150,16 @@ "properties": { "name": "Publish-BCModuleToTenant" } - } + }, + { + "id": "Enumerate-Environment", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "Enumerate-Environment" + } + } ] } \ No newline at end of file From 178aeea8f8ecddccc0c89964d9242006a8ca1eaa Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 14:33:02 -0600 Subject: [PATCH 114/130] Update file emission and win32 shield non-Windows agents --- .../function_Enumerate-Environment.js | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index ca8d968..be91fff 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -71,15 +71,20 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; // 4. Powershell version(s) let psVersion; let pwshVersion; - try { - psVersion = execSync( - `powershell -NoProfile -Command "$v = $PSVersionTable.PSVersion; Write-Output ($v.Major.ToString() + '.' + $v.Minor + '.' + $v.Build + '.' + $v.Revision)"`, - { encoding: 'utf8' } - ).trim(); + if (os.platform() === "win32") { + try { + psVersion = execSync( + `powershell -NoProfile -Command "$v = $PSVersionTable.PSVersion; Write-Output ($v.Major.ToString() + '.' + $v.Minor + '.' + $v.Build + '.' + $v.Revision)"`, + { encoding: 'utf8' } + ).trim(); + logger.info('[powershell version]:'.padEnd(logColWidth) + `${psVersion}`); + } catch (err) { + logger.error(`[powershell version]: Encountered an error while executing a 'powerhsell version'`); + logger.error(`[powershell version]: Error: ${err}`); + } + } else { + psVersion = "[not installed; Linux environment]"; logger.info('[powershell version]:'.padEnd(logColWidth) + `${psVersion}`); - } catch (err) { - logger.error(`[powershell version]: Encountered an error while executing a 'powerhsell version'`); - logger.error(`[powershell version]: Error: ${err}`); } try { @@ -96,20 +101,26 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; // 5. BCContainerHelper existence / version let result; let BCContainerHelperPresent = false; - try { - const psCommand = `$modulePath = Get-Module -ListAvailable BCContainerHelper | Select-Object -First 1 -ExpandProperty Path; if ($modulePath) { $psd1 = $modulePath -replace '\\[^\\\\]+$', '.psd1'; if (Test-Path $psd1) { $lines = Get-Content $psd1 -Raw; if ($lines -match 'ModuleVersion\\s*=\\s*[\\"\\'']?([0-9\\.]+)[\\"\\'']?') { Write-Output $matches[1]; } else { Write-Output '[version not found]'; } } else { Write-Output '[not installed]'; } } else { Write-Output '[not installed]'; }`; - result = execSync(`powershell.exe -NoProfile -Command "${psCommand.replace(/\n/g, ' ').replace(/"/g, '\\"')}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); - if (result === "") { result = '[not installed]'} - if (result && result != "") { - logger.info('[BCContainerHelper version]:'.padEnd(logColWidth) + `${result}`); + if (os.platform() === "win32") { + try { + const psCommand = `$modulePath = Get-Module -ListAvailable BCContainerHelper | Select-Object -First 1 -ExpandProperty Path; if ($modulePath) { $psd1 = $modulePath -replace '\\[^\\\\]+$', '.psd1'; if (Test-Path $psd1) { $lines = Get-Content $psd1 -Raw; if ($lines -match 'ModuleVersion\\s*=\\s*[\\"\\'']?([0-9\\.]+)[\\"\\'']?') { Write-Output $matches[1]; } else { Write-Output '[version not found]'; } } else { Write-Output '[not installed]'; } } else { Write-Output '[not installed]'; }`; + + result = execSync(`powershell.exe -NoProfile -Command "${psCommand.replace(/\n/g, ' ').replace(/"/g, '\\"')}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + if (result === "") { result = '[not installed]'} + if (result && result != "") { + logger.info('[BCContainerHelper version]:'.padEnd(logColWidth) + `${result}`); + BCContainerHelperPresent = true; + } BCContainerHelperPresent = true; - } - BCContainerHelperPresent = true; - } catch (err) { - logger.error(`[BCContainerHelper]: Failed to query module: ${err.message}`); - logger.info(err); - } + } catch (err) { + logger.error(`[BCContainerHelper]: Failed to query module: ${err.message}`); + logger.info(err); + } + } else { + result = "[not installed; Linux environment]"; + logger.info('[BCContainerHelper version]:'.padEnd(logColWidth) + `${result}`); + } // 6. Docker existence / version let DockerPresent = false; @@ -170,6 +181,13 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; // Deal with the file if requested (note it has already been parsed at the top of this routine) if (produceFile) { + let dockerList = []; + try { + dockerList = DockerPsObject.filter(img => img && img.Names).map(img => ({ name: img.Names, status: img.Status })); + } catch { + dockerList = []; + } + let candidateFile = { platform: os.platform(), whoami: textOut, @@ -178,7 +196,7 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; pscoreVersion: pwshVersion, bcContainerVersion: result, dockerVersion: DockerVersionResult, - dockerImages: DockerPsObject.filter(img => img && img.Names).map(img => ({ name: img.Names, status: img.Status })) + dockerImages: dockerList } let candidateFileString = JSON.stringify(candidateFile); From 32395bcf70a5e0fa7eba9135885ad99b7b735c98 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 14:57:59 -0600 Subject: [PATCH 115/130] Defend docker image enumeration when no images are available --- .../function_Enumerate-Environment.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index be91fff..140f061 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -161,12 +161,16 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; const lines = psResult.trim().split('\n'); DockerPsObject = lines.map(line => JSON.parse(line)); - logger.info('[dockerimage]:'.padEnd(logColWidth) + '**Name**'.padEnd(logColWidth) + '**Status**'); - DockerPsObject.forEach((image, idx) => { - if (image && image.name != "") { - logger.info('[dockerimage]:'.padEnd(logColWidth) + `${image.Names}`.padEnd(logColWidth) + `${image.Status}`); - } - }); + if (DockerPsObject.length > 0) { + logger.info('[dockerimage]:'.padEnd(logColWidth) + '**Name**'.padEnd(logColWidth) + '**Status**'); + DockerPsObject.forEach((image, idx) => { + if (image && image.name != "") { + logger.info('[dockerimage]:'.padEnd(logColWidth) + `${image.Names}`.padEnd(logColWidth) + `${image.Status}`); + } + }); + } else { + logger.info('[dockerimage]:'.padEnd(logColWidth) + '[no images]'); + } } catch (err) { const msg = err.message || ''; const stderr = err.stderr?.toString() || ''; From 9e7dd75fec3099328b40b48297c624c175152e8c Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 15:04:32 -0600 Subject: [PATCH 116/130] Extract pwsh version to quote-safe variable --- .../Enumerate-Environment/function_Enumerate-Environment.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index 140f061..c71763c 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -88,8 +88,11 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; } try { + const psCommand = `$v = $PSVersionTable.PSVersion; Write-Output "$($v.Major).$($v.Minor).$($v.Build).$($v.Revision)"`; + const quotedCommand = `'${psCommand.replace(/'/g, `'\\''`)}'`; + pwshVersion = execSync( - `pwsh -NoProfile -Command "$v = $PSVersionTable.PSVersion; Write-Output ($v.Major.ToString() + '.' + $v.Minor + '.' + $v.Patch)"`, + `pwsh -NoProfile -Command ${quotedCommand}`, { encoding: 'utf8' } ).trim(); logger.info('[pwsh version]:'.padEnd(logColWidth) + `${pwshVersion}`); From 28c2218f252e1b4b36e6df0722ec50c4333a999b Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 15:10:53 -0600 Subject: [PATCH 117/130] Better defense on pwsh version --- .../function_Enumerate-Environment.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index c71763c..0a04fb7 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -88,11 +88,14 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; } try { - const psCommand = `$v = $PSVersionTable.PSVersion; Write-Output "$($v.Major).$($v.Minor).$($v.Build).$($v.Revision)"`; + const psCommand = [ + '$v = $PSVersionTable.PSVersion;', + 'Write-Output ($v.Major.ToString() + \'.\' + $v.Minor + \'.\' + $v.Patch)' + ].join(' '); const quotedCommand = `'${psCommand.replace(/'/g, `'\\''`)}'`; pwshVersion = execSync( - `pwsh -NoProfile -Command ${quotedCommand}`, + `pwsh -NoProfile -Command "${psCommand}"`, { encoding: 'utf8' } ).trim(); logger.info('[pwsh version]:'.padEnd(logColWidth) + `${pwshVersion}`); From f957d90fddfbb0c7a072b9c7d05f166780fb40bc Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 15:23:58 -0600 Subject: [PATCH 118/130] Correct string interpolation across differing shell policies --- .../Enumerate-Environment/function_Enumerate-Environment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index 0a04fb7..fac5eeb 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -74,7 +74,7 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; if (os.platform() === "win32") { try { psVersion = execSync( - `powershell -NoProfile -Command "$v = $PSVersionTable.PSVersion; Write-Output ($v.Major.ToString() + '.' + $v.Minor + '.' + $v.Build + '.' + $v.Revision)"`, + `powershell -NoProfile -Command "$v = $PSVersionTable.PSVersion; Write-Output ('' + $v.Major + '.' + $v.Minor + '.' + $v.Build + '.' + $v.Revision)"`, { encoding: 'utf8' } ).trim(); logger.info('[powershell version]:'.padEnd(logColWidth) + `${psVersion}`); From b4bd6b32a3814235476e60691596ccef4759bea2 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 15:37:07 -0600 Subject: [PATCH 119/130] Increment version because .... reasons, I guess. --- _tasks/environments.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tasks/environments.json b/_tasks/environments.json index 585a4a0..0dfb84c 100644 --- a/_tasks/environments.json +++ b/_tasks/environments.json @@ -2,7 +2,7 @@ "version": { "major": 0, "minor": 1, - "patch": 9, + "patch": 10, "build": 0 }, "dev": { From bd9303ac7ab6e0ecf8d80f00e0c89b8827438aa1 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 15:42:08 -0600 Subject: [PATCH 120/130] Updated the wrong one with the right reason --- .../Enumerate-Environment/function_Enumerate-Environment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index fac5eeb..3649eb6 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -90,7 +90,7 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; try { const psCommand = [ '$v = $PSVersionTable.PSVersion;', - 'Write-Output ($v.Major.ToString() + \'.\' + $v.Minor + \'.\' + $v.Patch)' + 'Write-Output (\'\' + $v.Major.ToString() + \'.\' + $v.Minor + \'.\' + $v.Patch)' ].join(' '); const quotedCommand = `'${psCommand.replace(/'/g, `'\\''`)}'`; From 75d8fe7b86af2915c71800b88e7689a57abe6286 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 15:47:52 -0600 Subject: [PATCH 121/130] Still trying to trick the system --- .../Enumerate-Environment/function_Enumerate-Environment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index 3649eb6..1ce21e3 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -90,7 +90,7 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; try { const psCommand = [ '$v = $PSVersionTable.PSVersion;', - 'Write-Output (\'\' + $v.Major.ToString() + \'.\' + $v.Minor + \'.\' + $v.Patch)' + 'Write-Output ((\' \' + $v.Major + \'.\' + $v.Minor + \'.\' + $v.Patch).TrimStart())' ].join(' '); const quotedCommand = `'${psCommand.replace(/'/g, `'\\''`)}'`; From 64b14a98982dfa5c95751ae24f49444ba8fd3522 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 15:56:18 -0600 Subject: [PATCH 122/130] Try different quoting strategy --- .../Enumerate-Environment/function_Enumerate-Environment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index 1ce21e3..7f075cf 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -93,9 +93,9 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; 'Write-Output ((\' \' + $v.Major + \'.\' + $v.Minor + \'.\' + $v.Patch).TrimStart())' ].join(' '); const quotedCommand = `'${psCommand.replace(/'/g, `'\\''`)}'`; - + logger.debug(`psCommand: ${quotedCommand}`); pwshVersion = execSync( - `pwsh -NoProfile -Command "${psCommand}"`, + `pwsh -NoProfile -Command "${quotedCommand}"`, { encoding: 'utf8' } ).trim(); logger.info('[pwsh version]:'.padEnd(logColWidth) + `${pwshVersion}`); From e813284cafd9e5969ac2ec0c27f3aaf82768ddc9 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 16:20:58 -0600 Subject: [PATCH 123/130] Further escaping hell --- .../function_Enumerate-Environment.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index 7f075cf..512bf2d 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -90,12 +90,11 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; try { const psCommand = [ '$v = $PSVersionTable.PSVersion;', - 'Write-Output ((\' \' + $v.Major + \'.\' + $v.Minor + \'.\' + $v.Patch).TrimStart())' + 'Write-Output (\'\' + $v.Major + \'.\' + $v.Minor + \'.\' + $v.Patch).Trim()' ].join(' '); - const quotedCommand = `'${psCommand.replace(/'/g, `'\\''`)}'`; - logger.debug(`psCommand: ${quotedCommand}`); + const quotedCommand = `"${psCommand.replace(/"/g, '\\"')}"`; pwshVersion = execSync( - `pwsh -NoProfile -Command "${quotedCommand}"`, + `pwsh -NoProfile -Command ${quotedCommand}`, { encoding: 'utf8' } ).trim(); logger.info('[pwsh version]:'.padEnd(logColWidth) + `${pwshVersion}`); From aeb64a98ce8b114bb5ae585304a8921f05eef49a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 16:27:10 -0600 Subject: [PATCH 124/130] Why screw around when you can just use ToString()? --- .../function_Enumerate-Environment.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index 512bf2d..0676154 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -88,13 +88,10 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; } try { - const psCommand = [ - '$v = $PSVersionTable.PSVersion;', - 'Write-Output (\'\' + $v.Major + \'.\' + $v.Minor + \'.\' + $v.Patch).Trim()' - ].join(' '); - const quotedCommand = `"${psCommand.replace(/"/g, '\\"')}"`; + const psCommand = `"$PSVersionTable.PsVersion.ToString()"`; + //const quotedCommand = `"${psCommand.replace(/"/g, '\\"')}"`; pwshVersion = execSync( - `pwsh -NoProfile -Command ${quotedCommand}`, + `pwsh -NoProfile -Command ${psCommand}`, { encoding: 'utf8' } ).trim(); logger.info('[pwsh version]:'.padEnd(logColWidth) + `${pwshVersion}`); From aee00385f28d77bce9e1f169d4cf2929844aaf4a Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 16:35:31 -0600 Subject: [PATCH 125/130] Still trying to make this platform agnostic --- .../function_Enumerate-Environment.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index 0676154..92157d7 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -88,12 +88,15 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; } try { - const psCommand = `"$PSVersionTable.PsVersion.ToString()"`; + const isLinux = process.platform === 'linux'; + + const psCommandRaw = '$PSVersionTable.PSVersion.ToString()'; + const psCommand = isLinux + ? psCommandRaw.replace(/(["\\$`])/g, '\\$1') // escape for bash + : psCommandRaw; // don't escape on Windows + const fullCommand = `pwsh -NoProfile -Command "${psCommand}"`; //const quotedCommand = `"${psCommand.replace(/"/g, '\\"')}"`; - pwshVersion = execSync( - `pwsh -NoProfile -Command ${psCommand}`, - { encoding: 'utf8' } - ).trim(); + pwshVersion = execSync(fullCommand, { encoding: 'utf8' }).trim(); logger.info('[pwsh version]:'.padEnd(logColWidth) + `${pwshVersion}`); } catch (err) { logger.error(`[pwsh version]: Encountered an error while executing a 'pwsh version'`); From 5da2f173a6acb043877acd7c81374b5942de93cb Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 16:41:07 -0600 Subject: [PATCH 126/130] Clean up instance where docker images error when producing a file and no images available --- .../Enumerate-Environment/function_Enumerate-Environment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index 92157d7..cde99ed 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -164,7 +164,7 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; try { const psResult = execSync('docker ps -a --no-trunc --format "{{json .}}"', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); const lines = psResult.trim().split('\n'); - DockerPsObject = lines.map(line => JSON.parse(line)); + DockerPsObject = lines.filter(line => line && line.trim().startsWith('{') && line.trim().endsWith('}')).map(line => JSON.parse(line)); if (DockerPsObject.length > 0) { logger.info('[dockerimage]:'.padEnd(logColWidth) + '**Name**'.padEnd(logColWidth) + '**Status**'); From df87b0ede60cc19dfaaaf65d2836dedd5960d9be Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 16:50:14 -0600 Subject: [PATCH 127/130] Correct reference to filepath and name in function --- .../Enumerate-Environment/function_Enumerate-Environment.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index cde99ed..9b6e5a2 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -6,7 +6,7 @@ const { PassThrough } = require('stream'); const { logger, parseBool, getToken, normalizePath } = require(path.join(__dirname, '_common', 'CommonTools.js')); let produceFile = parseBool(process.env.INPUT_PRODUCEFILE); -const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; +const inputFilenameAndPath = process.env.INPUT_FILPATHANDNAME; // this routine is intended to provide information about the agent on which it is running // @@ -34,7 +34,7 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; logger.info(`Setting ProduceFile to ${produceFile}`); } } else { - logger.warn(`Requested a file output, but no file name and path was supplied in 'FilenameAndPath'`); + logger.warn(`Requested a file output, but no file name and path was supplied in 'FilePathAndName'`); produceFile = false; logger.info(`Setting ProduceFile to ${produceFile}`); } @@ -42,6 +42,7 @@ const inputFilenameAndPath = process.env.INPUT_FILENAMEANDPATH; logger.info('Invoking EGEnumerateEnvironment with the following parameters:'); logger.info('ProduceFile:'.padStart(2).padEnd(30) + `${produceFile}`); + logger.info('FilePathAndName:'.padStart(2).padEnd(30) + `${outputFilenameAndPath}`); logger.info(''); // 0. setup From 1a0884531daf7dd33cec4d1af609edc41af373df Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Tue, 17 Jun 2025 16:58:09 -0600 Subject: [PATCH 128/130] Typos, FTW! --- .../Enumerate-Environment/function_Enumerate-Environment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index 9b6e5a2..c684188 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -6,7 +6,7 @@ const { PassThrough } = require('stream'); const { logger, parseBool, getToken, normalizePath } = require(path.join(__dirname, '_common', 'CommonTools.js')); let produceFile = parseBool(process.env.INPUT_PRODUCEFILE); -const inputFilenameAndPath = process.env.INPUT_FILPATHANDNAME; +const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; // this routine is intended to provide information about the agent on which it is running // From 68f21bd38efbd815d40057c37059fd088c71eca2 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 12 Feb 2026 17:04:30 -0700 Subject: [PATCH 129/130] Incorporate new error condition into documentation Fixes #36 --- bc-tools-extension/README.md | 44 +++++++++++++----------- bc-tools-extension/RELEASE.md | 48 ++++++++++++++++----------- bc-tools-extension/vss-extension.json | 2 +- 3 files changed, 53 insertions(+), 41 deletions(-) diff --git a/bc-tools-extension/README.md b/bc-tools-extension/README.md index 341dffc..d30bd70 100644 --- a/bc-tools-extension/README.md +++ b/bc-tools-extension/README.md @@ -2,23 +2,26 @@ # Business Central Build Tasks for Azure DevOps - * [Overview](#overview) - * [Features](#features) - * [Installation](#installation) - * [Other Requirements](#other-requirements) - + [Azure AD App Registration](#azure-ad-app-registration) - + [Business Central Configuration](#business-central-configuration) - + [Setup Complete](#setup-complete) - * [Tasks Included](#tasks-included) - + [1. Get AL Compiler (`EGGetALCompiler`)](#1-get-al-compiler-eggetalcompiler) - + [2. Get AL Dependencies (`EGGetALDependencies`)](#2-get-al-dependencies-eggetaldependencies) - + [3. Build AL Package (`EGALBuildPackage`)](#3-build-al-package-egalbuildpackage) - + [4. Get List of Companies (`EGGetBCCompanies`)](#4-get-list-of-companies-eggetbccompanies) - + [5. Get List of Extensions (`EGGetBCModules`)](#5-get-list-of-extensions-eggetbcmodules) - + [6. Publish Extension to Business Central (`EGDeployBCModule`)](#6-publish-extension-to-business-central-egdeploybcmodule) - * [Example Pipeline](#example-pipeline) - * [Security & Trust](#security--trust) - * [Support](#support) +- [Business Central Build Tasks for Azure DevOps](#business-central-build-tasks-for-azure-devops) + - [Overview](#overview) + - [Features](#features) + - [Installation](#installation) + - [Other Requirements](#other-requirements) + - [Azure AD App Registration](#azure-ad-app-registration) + - [Business Central Configuration](#business-central-configuration) + - [Setup Complete](#setup-complete) + - [Tasks Included](#tasks-included) + - [1. Get AL Compiler (`EGGetALCompiler`)](#1-get-al-compiler-eggetalcompiler) + - [2. Get AL Dependencies (`EGGetALDependencies`)](#2-get-al-dependencies-eggetaldependencies) + - [3. Build AL Package (`EGALBuildPackage`)](#3-build-al-package-egalbuildpackage) + - [4. Get List of Companies (`EGGetBCCompanies`)](#4-get-list-of-companies-eggetbccompanies) + - [5. Get List of Extensions (`EGGetBCModules`)](#5-get-list-of-extensions-eggetbcmodules) + - [6. Publish Extension to Business Central (`EGDeployBCModule`)](#6-publish-extension-to-business-central-egdeploybcmodule) + - [Example Pipeline](#example-pipeline) + - [Common Failures](#common-failures) + - [Security \& Trust](#security--trust) + - [Support](#support) + - [License](#license) ## Overview @@ -311,9 +314,9 @@ There is not much more control that is provided and even the response codes from Here are some common failures and their likely causes: -|Failure|Cause| -|---|---| -|**Immediate fail on deploy** | An immediate failure often means the extension’s **version number hasn’t changed**. Business Central may retain internal metadata **even if** the extension was unpublished and removed. This can cause silent rejections during re-deploy. To confirm, try a manual upload — the web interface will usually surface an error message that the API silently swallows. | +|Failure|Cause|Corrective Action| +|---|---|---| +|**Immediate fail on deploy** | An immediate failure often means the extension’s **version number hasn’t changed**. Business Central may retain internal metadata **even if** the extension was unpublished and removed. This can cause silent rejections during re-deploy. To confirm, try a manual upload — the web interface will usually surface an error message that the API silently swallows. | Increment build number in ```app.json```. | | **Extension never installs / stuck in “InProgress”** | Business Central backend is overloaded or stalled (e.g., schema sync issues, queued deployments). | Increase `PollingTimeout` and check the BC admin center for other queued extensions or backend delays. | | **Extension fails with no visible error message** | The BC API may suppress detailed errors. Often due to invalid dependencies, permission errors, or duplicate version conflicts. | Try uploading the extension manually through the BC UI to surface hidden error messages. | | **Authentication fails** | Incorrect or expired `ClientSecret`; or app registration missing permissions. | Ensure app has Application permissions, `API.ReadWrite.All`, and admin consent granted. Rotate secret if expired. | @@ -322,6 +325,7 @@ Here are some common failures and their likely causes: | **Pipeline fails to find `app.json`** | Folder structure doesn't match expected default; `PathToAppJson` may be incorrect. | Confirm folder layout. Use an inline `ls` or echo step to validate paths before running. | | **Polling hangs until timeout** | Deployment didn’t register; call to `Microsoft.NAV.upload` may have silently failed. | Recheck that upload succeeded and bookmark is valid. Consider increasing logging verbosity. | | **ENOENT / ETAG errors during upload** | Missing `.app` file or stale `@odata.etag` from a previous upload attempt. | Confirm `Build Package` step ran and `.app` file exists. If needed, reacquire a fresh bookmark with valid ETAG. | +| **500 Internal Server Error** or **```"An error occurred while trying to download ''"```** during package download | The pipeline user may not have an entry in the Entra users in Business Central. | Add the pipeline user to the Entra users. | ## Security & Trust diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index 9939ba9..4011525 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -1,32 +1,40 @@ # Release Notes - BCBuildTasks Extension +- [Release Notes - BCBuildTasks Extension](#release-notes---bcbuildtasks-extension) +- [Version: 0.1.10](#version-0110) - [Version: 0.1.9](#version-019) - [Version: 0.1.8](#version-018) - [Version: 0.1.7](#version-017) + - [Fixes](#fixes) + - [Improvements](#improvements) - [Version: 0.1.6](#version-016) - [Version: 0.1.5](#version-015) - + [Feature Release](#feature-release) - * [New Features](#new-features) - + [1. **EGGetBCCompanies**](#1-eggetbccompanies) - + [2. **EGGetBCModules**](#2-eggetbcmodules) - + [3. **EGDeployBCModule**](#3-egdeploybcmodule) + - [Feature Release](#feature-release) + - [New Features](#new-features) + - [1. **EGGetBCCompanies**](#1-eggetbccompanies) + - [2. EGGetBCModules](#2-eggetbcmodules) + - [3. EGDeployBCModule](#3-egdeploybcmodule) - [Version: 0.1.4](#version-014) - + [Improvement Release](#improvement-release) - * [New Features](#new-features-1) - + [1. **EGGetALCompiler**](#1-eggetalcompiler) - + [2. **EGGetALDependencies**](#2-eggetaldependencies) - + [3. **EGBuildALPackage**](#3-egbuildalpackage) - * [Notes & Requirements](#notes--requirements) + - [Improvement Release](#improvement-release) + - [New Features](#new-features-1) + - [1. **EGGetALCompiler**](#1-eggetalcompiler) + - [2. **EGGetALDependencies**](#2-eggetaldependencies) + - [3. **EGBuildALPackage**](#3-egbuildalpackage) + - [Notes \& Requirements](#notes--requirements) - [Version: 0.1.0](#version-010) - + [Initial Release](#initial-release) - * [New Features](#new-features-2) - + [1. **EGGetALCompiler**](#1-eggetalcompiler-1) - + [2. **EGGetALDependencies**](#2-eggetaldependencies-1) - + [3. **EGBuildALPackage**](#3-egbuildalpackage-1) - * [Notes & Requirements](#notes--requirements-1) - * [Example Pipeline Usage](#example-pipeline-usage) - * [Known Limitations](#known-limitations) - * [Support](#support) + - [Initial Release](#initial-release) + - [New Features](#new-features-2) + - [1. **EGGetALCompiler**](#1-eggetalcompiler-1) + - [2. **EGGetALDependencies**](#2-eggetaldependencies-1) + - [3. **EGBuildALPackage**](#3-egbuildalpackage-1) + - [Notes \& Requirements](#notes--requirements-1) + - [Example Pipeline Usage](#example-pipeline-usage) + - [Known Limitations](#known-limitations) + - [Support](#support) + - [License](#license) + +# Version: 0.1.10 +- Adds [#36](https://github.com/crazycga/bcdevopsextension/issues/36): incorporate failure condition regarding missing pipeline user entry onto Microsoft Entra Application screen in Business Central. # Version: 0.1.9 - Addresses [#31](https://github.com/crazycga/bcdevopsextension/issues/31): ```EGGetALCompiler``` returning incorrect value. This update addresses this particular bug, having refactored the search / walk algorithm to find the compiler. diff --git a/bc-tools-extension/vss-extension.json b/bc-tools-extension/vss-extension.json index cc572d2..0fa2e8c 100644 --- a/bc-tools-extension/vss-extension.json +++ b/bc-tools-extension/vss-extension.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "id": "eg-bc-build-tasks", "name": "Business Central Build Tasks", - "version": "0.1.5", + "version": "0.1.10", "publisher": "Evergrowth", "targets": [ { From b1276622b9c55afb348abe71cb1baad6e5eac138 Mon Sep 17 00:00:00 2001 From: "jamesm@evergrowth.ca" Date: Thu, 12 Feb 2026 19:04:22 -0700 Subject: [PATCH 130/130] Add enumerate-environment and update documentation --- .../function_Enumerate-Environment.js | 49 +++++++------------ bc-tools-extension/README.md | 42 ++++++++++++++++ bc-tools-extension/RELEASE.md | 1 + 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js index c684188..ecbe475 100644 --- a/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js +++ b/bc-tools-extension/Enumerate-Environment/function_Enumerate-Environment.js @@ -5,7 +5,6 @@ const fs = require('fs'); const { PassThrough } = require('stream'); const { logger, parseBool, getToken, normalizePath } = require(path.join(__dirname, '_common', 'CommonTools.js')); -let produceFile = parseBool(process.env.INPUT_PRODUCEFILE); const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; // this routine is intended to provide information about the agent on which it is running @@ -20,41 +19,31 @@ const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; (async () => { let outputFilenameAndPath; - if (produceFile) { - if (inputFilenameAndPath && inputFilenameAndPath != '') { - outputFilenameAndPath = normalizePath(inputFilenameAndPath); - let pathInfo = path.parse(outputFilenameAndPath); - - let outputPath = pathInfo.dir; - let outputName = pathInfo.base; - - if (!outputName || !outputPath) { - logger.warn(`Requested a file output, but cannot parse the file name and path '${outputFilenameAndPath}'`); - produceFile = false; - logger.info(`Setting ProduceFile to ${produceFile}`); - } - } else { - logger.warn(`Requested a file output, but no file name and path was supplied in 'FilePathAndName'`); - produceFile = false; - logger.info(`Setting ProduceFile to ${produceFile}`); + + if (inputFilenameAndPath && inputFilenameAndPath.trim() !== '') { + outputFilenameAndPath = normalizePath(inputFilenameAndPath); + const pathInfo = path.parse(outputFilenameAndPath); + + if (!pathInfo.base || !pathInfo.dir) { + logger.warn(`Invalid file path supplied: '${outputFilenameAndPath}'. Skipping file production.`); + outputFilenameAndPath = undefined; } } logger.info('Invoking EGEnumerateEnvironment with the following parameters:'); - logger.info('ProduceFile:'.padStart(2).padEnd(30) + `${produceFile}`); logger.info('FilePathAndName:'.padStart(2).padEnd(30) + `${outputFilenameAndPath}`); logger.info(''); - + // 0. setup const logColWidth = 30; // 1. platform logger.info('[platform]:'.padEnd(logColWidth) + `${os.platform()}`); - + // 2. whoami - let textOut; + let textOut; try { - let whoami = execSync('whoami', { encoding: 'utf8'}); + let whoami = execSync('whoami', { encoding: 'utf8' }); textOut = whoami.toString().trim(); if (textOut.length > 0) { logger.info('[whoami]: '.padEnd(logColWidth) + `${textOut}`); @@ -113,7 +102,7 @@ const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; const psCommand = `$modulePath = Get-Module -ListAvailable BCContainerHelper | Select-Object -First 1 -ExpandProperty Path; if ($modulePath) { $psd1 = $modulePath -replace '\\[^\\\\]+$', '.psd1'; if (Test-Path $psd1) { $lines = Get-Content $psd1 -Raw; if ($lines -match 'ModuleVersion\\s*=\\s*[\\"\\'']?([0-9\\.]+)[\\"\\'']?') { Write-Output $matches[1]; } else { Write-Output '[version not found]'; } } else { Write-Output '[not installed]'; } } else { Write-Output '[not installed]'; }`; result = execSync(`powershell.exe -NoProfile -Command "${psCommand.replace(/\n/g, ' ').replace(/"/g, '\\"')}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); - if (result === "") { result = '[not installed]'} + if (result === "") { result = '[not installed]' } if (result && result != "") { logger.info('[BCContainerHelper version]:'.padEnd(logColWidth) + `${result}`); BCContainerHelperPresent = true; @@ -122,7 +111,7 @@ const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; } catch (err) { logger.error(`[BCContainerHelper]: Failed to query module: ${err.message}`); logger.info(err); - } + } } else { result = "[not installed; Linux environment]"; logger.info('[BCContainerHelper version]:'.padEnd(logColWidth) + `${result}`); @@ -133,7 +122,7 @@ const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; let DockerVersionResult; try { DockerResult = execSync('docker version --format "{{.Client.Version}}"', { stdio: ['pipe', 'pipe', 'pipe'] }); - if (DockerResult === "") { DockerVersionResult = '[not installed]'} + if (DockerResult === "") { DockerVersionResult = '[not installed]' } else { DockerVersionResult = DockerResult.toString().trim(); } if (DockerVersionResult && DockerVersionResult != "") { logger.info('[dockerversion]:'.padEnd(logColWidth) + `${DockerVersionResult}`); @@ -166,7 +155,7 @@ const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; const psResult = execSync('docker ps -a --no-trunc --format "{{json .}}"', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); const lines = psResult.trim().split('\n'); DockerPsObject = lines.filter(line => line && line.trim().startsWith('{') && line.trim().endsWith('}')).map(line => JSON.parse(line)); - + if (DockerPsObject.length > 0) { logger.info('[dockerimage]:'.padEnd(logColWidth) + '**Name**'.padEnd(logColWidth) + '**Status**'); DockerPsObject.forEach((image, idx) => { @@ -181,7 +170,7 @@ const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; const msg = err.message || ''; const stderr = err.stderr?.toString() || ''; - const combined = `${msg}\n${stderr}`; + const combined = `${msg}\n${stderr}`; logger.error(`[dockerimage]: Unexpected error: ${combined}`); } } else { @@ -189,7 +178,7 @@ const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; } // Deal with the file if requested (note it has already been parsed at the top of this routine) - if (produceFile) { + if (outputFilenameAndPath) { let dockerList = []; try { @@ -211,7 +200,7 @@ const inputFilenameAndPath = process.env.INPUT_FILEPATHANDNAME; let candidateFileString = JSON.stringify(candidateFile); fs.writeFileSync(outputFilenameAndPath, candidateFileString); - + logger.info(''); logger.info(`Produced file at: ${outputFilenameAndPath}`); } diff --git a/bc-tools-extension/README.md b/bc-tools-extension/README.md index d30bd70..aea7bbb 100644 --- a/bc-tools-extension/README.md +++ b/bc-tools-extension/README.md @@ -17,6 +17,7 @@ - [4. Get List of Companies (`EGGetBCCompanies`)](#4-get-list-of-companies-eggetbccompanies) - [5. Get List of Extensions (`EGGetBCModules`)](#5-get-list-of-extensions-eggetbcmodules) - [6. Publish Extension to Business Central (`EGDeployBCModule`)](#6-publish-extension-to-business-central-egdeploybcmodule) + - [7. Enumerate Environment (`EnumerateEnvironment`)](#7-enumerate-environment-enumerateenvironment) - [Example Pipeline](#example-pipeline) - [Common Failures](#common-failures) - [Security \& Trust](#security--trust) @@ -246,6 +247,42 @@ There is not much more control that is provided and even the response codes from |Input|`PollingFrequency`||`10`|The number of **seconds** to wait between attempts to poll the extension deployment status for information after the upload| |Input|`MaxPollingTimeout`||`600`|The maximum number of **seconds** to stop the pipeline to wait for the result of the deployment status; **note: use this value to prevent the pipeline from consuming too much time waiting for a response**| +### 7. Enumerate Environment (`EnumerateEnvironment`) + +This function returns information about the agent on which the pipeline is running for diagnostic and troubleshooting purposes. Included is: +* platform (windows or linux) +* whoami (user security context) +* current working directory +* Powershell version (if installed) +* pwsh version (if installed) +* BCContainerHelper version (if installed) +* docker version (if installed) +* list of docker images (if installed, and if any exist) + +The output can be put to a file for consumption by later steps in the pipeline. The output file is a JSON file, with the following format: + +```json +{ + "platform": "string", + "whoami": "string", + "workingDirectory": "string", + "powershellVersion": "string", + "pscoreVersion": "string", + "bcContainerVersion": "string", + "dockerVersion": "string", + "dockerImages": [ + { + "name": "string", + "status": "string" + } + ] +} +``` + +|Type|Name|Required|Default|Use| +|---|---|---|---|---| +|Input|FileNameAndPath||``|Directs where to save the file if `GenerateFile` is `true` | + ## Example Pipeline ```yaml @@ -308,6 +345,11 @@ There is not much more control that is provided and even the response codes from ClientSecret: "" CompanyId: "" +- task: EGEnumerateEnvironment@0 + displayName: "Enumerate environment" + inputs: + FileNameAndPath: ./environment.json + ``` ## Common Failures diff --git a/bc-tools-extension/RELEASE.md b/bc-tools-extension/RELEASE.md index 4011525..77b1bf4 100644 --- a/bc-tools-extension/RELEASE.md +++ b/bc-tools-extension/RELEASE.md @@ -35,6 +35,7 @@ # Version: 0.1.10 - Adds [#36](https://github.com/crazycga/bcdevopsextension/issues/36): incorporate failure condition regarding missing pipeline user entry onto Microsoft Entra Application screen in Business Central. +- Adds new function ```EGEnumerateEnvironment``` for fast troubleshooting of pipeline environments. # Version: 0.1.9 - Addresses [#31](https://github.com/crazycga/bcdevopsextension/issues/31): ```EGGetALCompiler``` returning incorrect value. This update addresses this particular bug, having refactored the search / walk algorithm to find the compiler.