diff --git a/appveyor.yml b/appveyor.yml index c25d05778e..a21f2b4c76 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -53,12 +53,13 @@ for: $hasTag = Test-Path env:APPVEYOR_REPO_TAG_NAME $isbuildFromMasterBranch = $env:APPVEYOR_REPO_BRANCH -eq "master" $includeSuffix = (-Not ($hasTag -OR $isBuildFromMasterBranch)) - + $bypassPackaging = $env:APPVEYOR_PULL_REQUEST_NUMBER -and -not $env:APPVEYOR_PULL_REQUEST_TITLE.Contains("[pack]") + if (-Not $includeSuffix) { $env:Configuration = "Release" } - .\build.ps1 -buildNumber "$env:APPVEYOR_BUILD_NUMBER" -includeSuffix $includeSuffix + .\build.ps1 -buildNumber "$env:APPVEYOR_BUILD_NUMBER" -includeSuffix $includeSuffix -$bypassPackaging $bypassPackaging after_build: - ps: > $bypassPackaging = $env:APPVEYOR_PULL_REQUEST_NUMBER -and -not $env:APPVEYOR_PULL_REQUEST_TITLE.Contains("[pack]") diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000000..366a7138dc --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,394 @@ +variables: + buildNumber: $[ counter('constant', 13000) ] # Start higher than our AppVeyor versions. Every build (pr or branch) will increment. + +name: 2.0.$(buildNumber) + +pr: + branches: + include: + - master + - dev + +trigger: + branches: + include: + - master + - dev + +jobs: +- job: InitializePipeline + pool: + vmImage: 'vs2017-win2016' + steps: + - task: AzureKeyVault@1 + inputs: + # Note: This is actually a Service Connection in DevOps, not an Azure subscription name + azureSubscription: 'Azure-Functions-Host-CI' + keyVaultName: 'azure-functions-host-ci' + secretsFilter: '*' + - task: PowerShell@2 + displayName: 'Initialize' + name: Initialize + inputs: + filePath: '$(Build.Repository.LocalPath)\build\initialize-pipeline.ps1' + +- job: BuildArtifacts_Ubuntu + pool: + vmImage: 'ubuntu-18.04' + steps: + - task: Bash@3 + inputs: + targetType: 'inline' + script: | + mkdir .dotnet && + chmod +x dotnet-install.sh && + ./dotnet-install.sh --version 2.2.202 --install-dir .dotnet && + PATH=".dotnet:"$PATH && dotnet --info + dotnet build WebJobs.Script.sln + +- job: BuildArtifacts_Windows + dependsOn: InitializePipeline + variables: + bypassPackaging: $[ dependencies.InitializePipeline.outputs['Initialize.BypassPackaging'] ] + includeSuffix: $[ dependencies.InitializePipeline.outputs['Initialize.IncludeSuffix'] ] + condition: and(succeeded(), eq(variables['bypassPackaging'], false)) + pool: + vmImage: 'vs2017-win2016' + steps: + - task: UseDotNet@2 + inputs: + packageType: 'sdk' + version: '2.2.x' + performMultiLevelLookup: true + - task: PowerShell@2 + displayName: "Build artifacts" + inputs: + filePath: '$(Build.Repository.LocalPath)\build.ps1' + arguments: '-buildNumber "$(buildNumber)" -includeSuffix ([System.Convert]::ToBoolean("$(includeSuffix)")) -bypassPackaging $false -signOutput $false' + - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + displayName: 'ESRP CodeSigning: Strong Name and Authenticode' + inputs: + ConnectedServiceName: 'ESRP Service' + FolderPath: 'tools\ExtensionsMetadataGenerator\src\ExtensionsMetadataGenerator\bin\Release' + Pattern: Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator*.dll + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode" : "CP-233863-SN", + "OperationCode" : "StrongNameSign", + "Parameters" : {}, + "ToolName" : "sign", + "ToolVersion" : "1.0" + }, + { + "KeyCode" : "CP-233863-SN", + "OperationCode" : "StrongNameVerify", + "Parameters" : {}, + "ToolName" : "sign", + "ToolVersion" : "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "http://www.microsoft.com", + "FileDigest": "/fd \"SHA256\"", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + - task: DeleteFiles@1 + displayName: 'Delete CodeSignSummary files' + inputs: + contents: '**\CodeSignSummary-*.md' + - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + displayName: 'ESRP CodeSigning: Nupkg' + inputs: + ConnectedServiceName: 'ESRP Service' + FolderPath: '..\..\..\buildoutput' + Pattern: 'Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator*.nupkg' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + - task: DeleteFiles@1 + displayName: 'Delete CodeSignSummary files' + inputs: + contents: '**\CodeSignSummary-*.md' + - task: CopyFiles@2 + inputs: + SourceFolder: '..\..\..\buildoutput' + Contents: '*.nupkg' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + - task: CopyFiles@2 + inputs: + SourceFolder: '..\..\buildoutput' + Contents: '*.nupkg' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + - task: CopyFiles@2 + inputs: + SourceFolder: '$(Build.Repository.LocalPath)\buildoutput' + Contents: '*.zip' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + - publish: $(Build.ArtifactStagingDirectory) + artifact: artifacts + +- job: RunUnitTests + pool: + vmImage: 'vs2017-win2016' + steps: + - task: UseDotNet@2 + inputs: + packageType: 'sdk' + version: '2.2.x' + performMultiLevelLookup: true + - task: DotNetCoreCLI@2 + displayName: 'Unit Tests' + inputs: + command: 'test' + testRunTitle: 'Unit Tests' + arguments: '-v n' + projects: | + **\ExtensionsMetadataGeneratorTests.csproj + **\WebJobs.Script.Scaling.Tests.csproj + **\WebJobs.Script.Tests.csproj + +- job: RunNonE2EIntegrationTests + pool: + vmImage: 'vs2017-win2016' + steps: + - task: UseDotNet@2 + inputs: + packageType: 'sdk' + version: '2.2.x' + performMultiLevelLookup: true + - task: UseNode@1 + inputs: + version: '10.x' + - task: PowerShell@2 + displayName: 'Install Az.Storage Powershell module' + inputs: + targetType: 'inline' + script: 'Install-Module -Name Az.Storage -RequiredVersion 1.11.0 -Scope CurrentUser -Force -AllowClobber' + - task: AzureKeyVault@1 + inputs: + # Note: This is actually a Service Connection in DevOps, not an Azure subscription name + azureSubscription: 'Azure-Functions-Host-CI' + keyVaultName: 'azure-functions-host-ci' + secretsFilter: '*' + - task: PowerShell@2 + displayName: 'Checkout secrets' + inputs: + filePath: '$(Build.Repository.LocalPath)\build\checkout-secrets.ps1' + arguments: '-connectionString ''$(Storage-azurefunctionshostci0)''' + - task: AzureKeyVault@1 + inputs: + # Note: This is actually a Service Connection in DevOps, not an Azure subscription name + azureSubscription: 'Azure-Functions-Host-CI' + keyVaultName: azure-functions-host-$(LeaseBlob) + secretsFilter: '*' + - task: DotNetCoreCLI@2 + displayName: 'Non-E2E integration tests' + inputs: + command: 'test' + testRunTitle: 'Non-E2E integration tests' + arguments: '--filter "Category!=E2E"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + env: + AzureWebJobsStorage: $(Storage) + ConnectionStrings__CosmosDB: $(CosmosDB) + AzureWebJobsEventHubSender: $(EventHub) + AzureWebJobsEventHubReceiver: $(EventHub) + AzureWebJobsSecretStorageKeyVaultConnectionString: $(KeyVaultConnectionString) + AzureWebJobsSecretStorageKeyVaultName: $(KeyVaultName) + - task: PowerShell@2 + condition: always() + displayName: 'Checkin secrets' + inputs: + filePath: '$(Build.Repository.LocalPath)\build\checkin-secrets.ps1' + arguments: '-connectionString ''$(Storage-azurefunctionshostci0)'' -leaseBlob $(LeaseBlob) -leaseToken $(LeaseToken)' + +- job: RunIntegrationTests + pool: + vmImage: 'vs2017-win2016' + steps: + - task: UseDotNet@2 + inputs: + packageType: 'sdk' + version: '2.2.x' + performMultiLevelLookup: true + - task: UseNode@1 + inputs: + version: '10.x' + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.7.x' + addToPath: true + - task: PowerShell@2 + displayName: 'Install Az.Storage Powershell module' + inputs: + targetType: 'inline' + script: 'Install-Module -Name Az.Storage -RequiredVersion 1.11.0 -Scope CurrentUser -Force -AllowClobber' + - task: AzureKeyVault@1 + inputs: + # Note: This is actually a Service Connection in DevOps, not an Azure subscription name + azureSubscription: 'Azure-Functions-Host-CI' + keyVaultName: 'azure-functions-host-ci' + secretsFilter: '*' + - task: PowerShell@2 + displayName: 'Checkout secrets' + inputs: + filePath: '$(Build.Repository.LocalPath)\build\checkout-secrets.ps1' + arguments: '-connectionString ''$(Storage-azurefunctionshostci0)''' + - task: AzureKeyVault@1 + inputs: + # Note: This is actually a Service Connection in DevOps, not an Azure subscription name + azureSubscription: 'Azure-Functions-Host-CI' + keyVaultName: azure-functions-host-$(LeaseBlob) + secretsFilter: '*' + - task: PowerShell@2 + displayName: 'Set environment variables' + inputs: + targetType: 'inline' + script: | + Write-Host "##vso[task.setvariable variable=AzureWebJobsStorage]$env:AzureWebJobsStorageSecretMap" + Write-Host "##vso[task.setvariable variable=ConnectionStrings__CosmosDB]$env:CosmosDbSecretMap" + Write-Host "##vso[task.setvariable variable=AzureWebJobsEventHubSender]$env:AzureWebJobsEventHubSenderSecretMap" + Write-Host "##vso[task.setvariable variable=AzureWebJobsEventHubReceiver]$env:AzureWebJobsEventHubReceiverSecretMap" + env: + AzureWebJobsStorageSecretMap: $(Storage) + CosmosDbSecretMap: $(CosmosDb) + AzureWebJobsEventHubSenderSecretMap: $(EventHub) + AzureWebJobsEventHubReceiverSecretMap: $(EventHub) + - task: DotNetCoreCLI@2 + displayName: "C# end to end tests" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "C# end to end tests" + arguments: '--filter "Group=CSharpEndToEndTests"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "Node end to end tests" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "Node end to end tests" + arguments: '--filter "Group=NodeEndToEndTests"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "Direct load end to end tests" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "Direct load end to end tests" + arguments: '--filter "Group=DirectLoadEndToEndTests"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "F# end to end tests" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "F# end to end tests" + arguments: '--filter "Group=FSharpEndToEndTests"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "Language worker end to end tests" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "Language worker end to end tests" + arguments: '--filter "Group=LanguageWorkerSelectionEndToEndTests"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "Node script host end to end tests" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "Node script host end to end tests" + arguments: '--filter "Group=NodeScriptHostTests"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "Raw assembly end to end tests" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "Raw assembly end to end tests" + arguments: '--filter "Group=RawAssemblyEndToEndTests"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "Samples end to end tests" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "Samples end to end tests" + arguments: '--filter "Group=SamplesEndToEndTests"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "Standby mode end to end tests Windows" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "Standby mode end to end tests Windows" + arguments: '--filter "Group=StandbyModeEndToEndTests_Windows"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "Standby mode end to end tests Linux" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "Standby mode end to end tests Linux" + arguments: '--filter "Group=StandbyModeEndToEndTests_Linux"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "Linux container end to end tests Windows" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "Linux container end to end tests Windows" + arguments: '--filter "Group=ContainerInstanceTests"' + projects: | + **\WebJobs.Script.Tests.Integration.csproj + - task: PowerShell@2 + condition: always() + displayName: 'Checkin secrets' + inputs: + filePath: '$(Build.Repository.LocalPath)\build\checkin-secrets.ps1' + arguments: '-connectionString ''$(Storage-azurefunctionshostci0)'' -leaseBlob $(LeaseBlob) -leaseToken $(LeaseToken)' \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 450b70358a..9aa19764ab 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,13 +1,19 @@ param ( [string]$buildNumber = "0", [string]$extensionVersion = "2.0.$buildNumber", - [bool]$includeSuffix = $true + [bool]$includeSuffix = $true, + [bool]$bypassPackaging = $true, + [bool]$signOutput = $true ) if ($includeSuffix) { $extensionVersion += "-prerelease" } +$sourceBranch = $env:BUILD_SOURCEBRANCH +Write-Host "Bypass packaging: $bypassPackaging" +Write-Host "IncludeSuffix: $includeSuffix" +Write-Host "SourceBranch: $sourceBranch" $currentDir = Split-Path -Parent $MyInvocation.MyCommand.Path $buildOutput = Join-Path $currentDir "buildoutput" @@ -43,7 +49,7 @@ function CrossGen([string] $runtime, [string] $publishTarget, [string] $privateS dotnet publish .\src\WebJobs.Script.WebHost\WebJobs.Script.WebHost.csproj -r $runtime -o "$selfContained" -v q /p:BuildNumber=$buildNumber # Modify web.config for inproc - dotnet tool install -g dotnet-xdt --version 2.1.0 2> $null + dotnet tool install -g dotnet-xdt --version 2.1.0 | Out-Null dotnet-xdt -s "$privateSiteExtensionPath\web.config" -t "$privateSiteExtensionPath\web.InProcess.$runtime.xdt" -o "$privateSiteExtensionPath\web.config" $successfullDlls =@() @@ -101,7 +107,18 @@ function AddDiaSymReaderToPath() $_ -replace '\s+Base Path:','' } - $diaSymPath = Join-Path $sdkBasePath.Trim() "Roslyn\bincore\runtimes\win\native" + $parent = Split-Path -Path $sdkBasePath.Trim() + $maxValue = 0 + Get-ChildItem $parent\2.2.* | + ForEach-Object { + $newVal = $_.Extension -replace '\.','' + if($newVal -gt $maxValue) { + $maxValue = $newVal + } + } + + $finalPath = $parent + "\2.2.$maxValue" + $diaSymPath = Join-Path $finalPath.Trim() "Roslyn\bincore\runtimes\win\native" Write-Host "Adding DiaSymReader location to path ($diaSymPath)" -ForegroundColor Yellow $env:Path = "$diaSymPath;$env:Path" @@ -233,9 +250,6 @@ function CreateZips([string] $runtimeSuffix) { function deleteDuplicateWorkers() { Write-Host "Deleting workers directory: $privateSiteExtensionPath\32bit\workers" Remove-Item -Recurse -Force "$privateSiteExtensionPath\32bit\workers" -ErrorAction SilentlyContinue - Write-Host "Moving workers directory:$privateSiteExtensionPath\64bit\workers to" $privateSiteExtensionPath - - Move-Item -Path "$privateSiteExtensionPath\64bit\workers" -Destination "$privateSiteExtensionPath\workers" } function cleanExtension([string] $bitness) { @@ -259,6 +273,10 @@ function cleanExtension([string] $bitness) { dotnet --version dotnet build .\WebJobs.Script.sln -v q /p:BuildNumber="$buildNumber" +if($LASTEXITCODE -ne 0) { + throw "Build failed" +} + $projects = "WebJobs.Script", "WebJobs.Script.WebHost", @@ -278,8 +296,6 @@ $cmd = "pack", "tools\WebJobs.Script.Performance\WebJobs.Script.Performance.App\ $cmd = "pack", "tools\ExtensionsMetadataGenerator\src\ExtensionsMetadataGenerator\ExtensionsMetadataGenerator.csproj", "-o", "..\..\..\..\buildoutput", "-c", "Release" & dotnet $cmd -$bypassPackaging = $env:APPVEYOR_PULL_REQUEST_NUMBER -and -not $env:APPVEYOR_PULL_REQUEST_TITLE.Contains("[pack]") - if ($bypassPackaging){ Write-Host "Bypassing artifact packaging and CrossGen for pull request." -ForegroundColor Yellow } else { @@ -291,6 +307,8 @@ if ($bypassPackaging){ #build win-x86 and win-x64 extension BuildPackages 0 - & ".\tools\RunSigningJob.ps1" + if($signOutput) { + & ".\tools\RunSigningJob.ps1" + } if (-not $?) { exit 1 } -} \ No newline at end of file +} diff --git a/build/checkin-secrets.ps1 b/build/checkin-secrets.ps1 new file mode 100644 index 0000000000..b7bea88f1e --- /dev/null +++ b/build/checkin-secrets.ps1 @@ -0,0 +1,24 @@ +param ( + [string]$connectionString = "", + [string]$leaseBlob = "", + [string]$leaseToken = "" +) + +if ($leaseBlob -eq "") { + Write-Host "leaseBlob was not specified." + exit 1 +} + +if ($leaseToken -eq "") { + Write-Host "leaseToken was not specified." + exit 1 +} + +Write-Host "Breaking lease for $leaseBlob." + +$storageContext = New-AzStorageContext -ConnectionString $connectionString +$blob = Get-AzStorageBlob -Context $storageContext -Container "ci-locks" -Blob $leaseBlob + +$accessCondition = New-Object -TypeName Microsoft.Azure.Storage.AccessCondition +$accessCondition.LeaseId = $leaseToken +$blob.ICloudBlob.ReleaseLease($accessCondition) \ No newline at end of file diff --git a/build/checkout-secrets.ps1 b/build/checkout-secrets.ps1 new file mode 100644 index 0000000000..1b5c9e5d92 --- /dev/null +++ b/build/checkout-secrets.ps1 @@ -0,0 +1,75 @@ +param ( + [string]$connectionString = "" +) + +function AcquireLease($blob) { + try { + return $blob.ICloudBlob.AcquireLease($null, $null, $null, $null, $null) + } catch { + Write-Host " Error: $_" + return $null + } +} + +# use this for tracking metadata in lease blobs +$buildName = "2.0." + $env:buildNumber + "_" + $env:SYSTEM_JOBDISPLAYNAME + +$azVersion = "1.11.0" +Import-Module Az.Storage +$azModule = Get-Module -Name Az.Storage +if ($azModule.Version -ne $azVersion) { + throw "Az.Storage module version $azVersion was not found. Current version: $($azModule.Version)" +} + +# get a blob lease to prevent test overlap +$storageContext = New-AzStorageContext -ConnectionString $connectionString + +While($true) { + $blobs = Get-AzStorageBlob -Context $storageContext -Container "ci-locks" + $token = $null + + # shuffle the blobs for random ordering + $blobs = $blobs | Sort-Object {Get-Random} + + Write-Host "Looking for unleased ci-lock blobs (list is shuffled):" + Foreach ($blob in $blobs) { + $name = $blob.Name + $leaseStatus = $blob.ICloudBlob.Properties.LeaseStatus + + Write-Host " ${name}: $leaseStatus" + + if ($leaseStatus -eq "Locked") { + continue + } + + Write-Host " Attempting to acquire lease on $name." + $token = AcquireLease $blob + if ($token -ne $null) { + Write-Host " Lease acquired on $name. LeaseId: '$token'" + Write-Host "##vso[task.setvariable variable=LeaseBlob]$name" + Write-Host "##vso[task.setvariable variable=LeaseToken]$token" + try { + $blob.ICloudBlob.FetchAttributes() + $blob.ICloudBlob.Metadata["Build"] = $buildName + $accessCondition = New-Object -TypeName Microsoft.Azure.Storage.AccessCondition + $accessCondition.LeaseId = $token + $blob.ICloudBlob.SetMetadata($accessCondition) + } catch { + # best effort + Write-Host "Warning: unable to update blob metadata. Continuing. $_" + } + break + } else { + Write-Host " Lease not acquired on $name." + } + } + + if ($token -ne $null) { + break + } + + $delay = 30 + Write-Host "No lease acquired. Waiting $delay seconds to try again. This run cannot begin until it acquires a lease on a CI test environment." + Start-Sleep -s $delay + Write-Host "" +} diff --git a/build/initialize-pipeline.ps1 b/build/initialize-pipeline.ps1 new file mode 100644 index 0000000000..6db1773691 --- /dev/null +++ b/build/initialize-pipeline.ps1 @@ -0,0 +1,23 @@ +$buildReason = $env:BUILD_REASON +$sourceBranch = $env:BUILD_SOURCEBRANCH +$bypassPackaging = $true +$includeSuffix = $true +Write-Host "SourceBranch: $sourceBranch, Build reason: $buildReason" + +if($sourceBranch.endsWith('master') -and ($buildReason -ne "PullRequest")) +{ + $includeSuffix = $false + $bypassPackaging = $false +} +elseif($buildReason -eq "PullRequest") +{ + $response = Invoke-RestMethod api.github.com/repos/$env:BUILD_REPOSITORY_ID/pulls/$env:SYSTEM_PULLREQUEST_PULLREQUESTNUMBER + $title = $response.title.ToLowerInvariant() + if ($title.Contains("[pack]")) { + $bypassPackaging = $false + } +} + +# Write to output +"##vso[task.setvariable variable=IncludeSuffix;isOutput=true]$includeSuffix" +"##vso[task.setvariable variable=BypassPackaging;isOutput=true]$bypassPackaging" \ No newline at end of file