diff --git a/.azure-pipelines/build-job.yml b/.azure-pipelines/build-job.yml index 76d4b4977f..570b12f099 100644 --- a/.azure-pipelines/build-job.yml +++ b/.azure-pipelines/build-job.yml @@ -84,10 +84,10 @@ jobs: ${{ if eq(parameters.targetFramework, 'all') }}: strategy: matrix: - NET6.0: - targetFramework: 'net6.0' NET8.0: targetFramework: 'net8.0' + NET10.0: + targetFramework: 'net10.0' ${{ if ne(parameters.container, '') }}: @@ -218,10 +218,12 @@ jobs: # Code coverage - ${{ if and(parameters.codeCoverage, parameters.unitTests, parameters.functionalTests) }}: - - script: dotnet tool install --global dotnet-reportgenerator-globaltool - displayName: Install Report Generator + - script: | + dotnet tool install --global dotnet-reportgenerator-globaltool + dotnet tool install --global dotnet-coverage + displayName: Install Code Coverage and Report Generator - - script: ${{ variables.devCommand }} report + - script: "${{ variables.devCommand }} report $(targetFramework) Debug ${{ parameters.os }}-${{ parameters.arch }}" displayName: Generate Code Coverage report workingDirectory: src @@ -272,7 +274,7 @@ jobs: path: ${{ variables.layoutRoot }} # Verify all binaries are signed (check report) - - task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@1 + - task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@2 displayName: 'Verify Codesign Report' inputs: CodesignValidation: true diff --git a/.azure-pipelines/pipeline.yml b/.azure-pipelines/pipeline.yml index 2f0f8c0a68..5ca9a7d3c8 100644 --- a/.azure-pipelines/pipeline.yml +++ b/.azure-pipelines/pipeline.yml @@ -113,13 +113,13 @@ extends: ${{ if eq(parameters.targetFramework, 'all') }}: strategy: matrix: - NET6.0: - targetFramework: 'net6.0' NET8.0: targetFramework: 'net8.0' + NET10.0: + targetFramework: 'net10.0' pool: name: 1ES-ABTT-Shared-Pool - image: abtt-ubuntu-2204 + image: abtt-ubuntu-2404 os: linux variables: DisableDockerDetector: true @@ -129,9 +129,12 @@ extends: timeoutInMinutes: 300 steps: - template: /.azure-pipelines/get-pat.yml@self + - task: NpmAuthenticate@0 + inputs: + workingFile: .azure-pipelines/scripts/.npmrc - bash: | cd ./.azure-pipelines/scripts/ - npm install axios minimist + npm ci --ignore-scripts releaseBranch="${{ parameters.branch }}" sourceBranch="$(Build.SourceBranch)" @@ -216,7 +219,7 @@ extends: displayName: Linux (x64) pool: name: 1ES-ABTT-Shared-Pool - image: abtt-ubuntu-2204 + image: abtt-ubuntu-2404 os: linux os: linux arch: x64 @@ -236,7 +239,7 @@ extends: displayName: Linux (ARM) pool: name: 1ES-ABTT-Shared-Pool - image: abtt-ubuntu-2204 + image: abtt-ubuntu-2404 os: linux timeoutInMinutes: 75 os: linux @@ -257,7 +260,7 @@ extends: displayName: Linux (ARM64) pool: name: 1ES-ABTT-Shared-ARM-64-Pool - vmImage: abtt-mariner_arm64 + vmImage: abtt-azurelinux3_arm64 os: linux timeoutInMinutes: 75 os: linux @@ -278,7 +281,7 @@ extends: displayName: Alpine (x64) pool: name: 1ES-ABTT-Shared-Pool - image: abtt-ubuntu-2204 + image: abtt-ubuntu-2404 os: linux #container: alpine os: linux-musl @@ -299,7 +302,7 @@ extends: displayName: Alpine (ARM64) pool: name: 1ES-ABTT-Shared-ARM-64-Pool - vmImage: abtt-mariner_arm64 + vmImage: abtt-azurelinux3_arm64 os: linux # container: # arm64v8/alpine (N/A) os: linux-musl @@ -321,7 +324,7 @@ extends: displayName: macOS (x64) pool: name: Azure Pipelines - image: macos-14 + image: macos-15 os: macOS os: osx arch: x64 @@ -342,7 +345,7 @@ extends: displayName: macOS (ARM64) pool: name: Azure Pipelines - image: macos-14 + image: macos-15-arm64 os: macOS os: osx arch: arm64 diff --git a/.azure-pipelines/scripts/.npmrc b/.azure-pipelines/scripts/.npmrc new file mode 100644 index 0000000000..615a78f4e4 --- /dev/null +++ b/.azure-pipelines/scripts/.npmrc @@ -0,0 +1,3 @@ +registry=https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/ + +always-auth=true diff --git a/.azure-pipelines/scripts/package-lock.json b/.azure-pipelines/scripts/package-lock.json new file mode 100644 index 0000000000..68ab1c7c88 --- /dev/null +++ b/.azure-pipelines/scripts/package-lock.json @@ -0,0 +1,307 @@ +{ + "name": "azure-pipelines-canary-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "azure-pipelines-canary-scripts", + "version": "1.0.0", + "dependencies": { + "axios": "1.16.0", + "minimist": "1.2.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/axios/-/axios-1.16.0.tgz", + "integrity": "sha1-+OXdkxzvKl+MMiFtV4Ttovh1Drc=", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha1-S1QowiK+mF15w9gmV0edvgtZstY=", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha1-w9RaizT9cwYxoRCoolIGgrMdWn8=", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha1-165mfh3INIL4tw/Q9u78UNow9Yo=", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha1-mD6y+aZyTpMD9hrd8BHHLgngsPo=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha1-BfdaJdq5jk+x3NXhRywFRtUFfI8=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha1-HE8sSDcydZfOadLKGQp/3RcjOME=", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha1-8x274MGDsAptJutjJcgQwP0YvU0=", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha1-KEdKFZ07nRHvYgUKFO1g5N9tYbw=", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha1-tJ5IhYBF/0y/awPhgFzrytNnkFM=", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha1-LALYZNl/PqbIgwxGTL0Rq26rehw=", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha1-dD8OO2lkqTpUke0b/6rgVNf5jQE=", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha1-FQs/J0OGnvPoUewMSdFbHRTQDuE=", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha1-ifVrghe9vIgCvSmd9tfxCB1+UaE=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha1-/JxqeDoISVHQuXH+EBjegTcHozg=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha1-LNxC1AvvLltO6rfAGnPFTOerWrw=", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha1-XlwrFbYDcKTHkww4Pft2vxe8QDw=", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha1-oN10voHiqlwvJ+Zc4oNgXuTit/k=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha1-u6vNwChZ9JhzAchW4zh85exDv3A=", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha1-OBqHG2KnNEUGYK497uRIE/cNlZo=", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha1-waRk52kzAuCCoHXO4MBXdBrEdyw=", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha1-p0h1aK2tV3z6qn6IxJyrOrMIGro=", + "license": "MIT", + "engines": { + "node": ">=10" + } + } + } +} diff --git a/.azure-pipelines/scripts/package.json b/.azure-pipelines/scripts/package.json new file mode 100644 index 0000000000..195596ab64 --- /dev/null +++ b/.azure-pipelines/scripts/package.json @@ -0,0 +1,10 @@ +{ + "name": "azure-pipelines-canary-scripts", + "version": "1.0.0", + "private": true, + "description": "CI scripts for Azure Pipelines Agent canary tests", + "dependencies": { + "axios": "1.16.0", + "minimist": "1.2.8" + } +} diff --git a/.editorconfig b/.editorconfig index e0c9ec240f..cc9ca1c448 100644 --- a/.editorconfig +++ b/.editorconfig @@ -817,6 +817,71 @@ dotnet_diagnostic.CA5403.severity = error # Analyzer version mismatch dotnet_diagnostic.CA9999.severity = error +# Microsoft.VisualStudio.Threading.Analyzers rules + +# Avoid async void methods +dotnet_diagnostic.VSTHRD100.severity = error + +# Synchronously waiting on tasks (.Result, .Wait()) — 54 existing violations, fix incrementally +dotnet_diagnostic.VSTHRD002.severity = suggestion + +# Awaiting/returning tasks from non-async context — 30 existing violations, fix incrementally +dotnet_diagnostic.VSTHRD003.severity = suggestion + +# Invoke single-threaded types on Main thread +dotnet_diagnostic.VSTHRD010.severity = warning + +# Use AsyncLazy +dotnet_diagnostic.VSTHRD011.severity = warning + +# Provide JoinableTaskFactory where allowed +dotnet_diagnostic.VSTHRD012.severity = warning + +# Async lambda for void-returning delegate — 1 existing violation, fix incrementally +dotnet_diagnostic.VSTHRD101.severity = suggestion + +# Implement internal logic as async method +dotnet_diagnostic.VSTHRD102.severity = warning + +# Call async methods when in an async method — 35 existing violations, fix incrementally +dotnet_diagnostic.VSTHRD103.severity = suggestion + +# Offer async methods — 5 existing violations, fix incrementally +dotnet_diagnostic.VSTHRD104.severity = suggestion + +# Avoid method overloads that assume TaskScheduler.Current — 2 existing violations, fix incrementally +dotnet_diagnostic.VSTHRD105.severity = suggestion + +# Use InvokeAsync to raise async events +dotnet_diagnostic.VSTHRD106.severity = warning + +# Observe awaitable values in object initializers +dotnet_diagnostic.VSTHRD107.severity = warning + +# Assert thread affinity unconditionally +dotnet_diagnostic.VSTHRD108.severity = warning + +# Switch instead of assert in async methods +dotnet_diagnostic.VSTHRD109.severity = warning + +# Observe result of async calls — 7 existing violations, fix incrementally +dotnet_diagnostic.VSTHRD110.severity = suggestion + +# Use .ConfigureAwait(bool) — silenced to align with CA2007=silent (app-level code, no SynchronizationContext) +dotnet_diagnostic.VSTHRD111.severity = silent + +# Implement System.IAsyncDisposable +dotnet_diagnostic.VSTHRD112.severity = warning + +# Use CancellationToken in awaited methods +dotnet_diagnostic.VSTHRD113.severity = warning + +# Avoid returning null from a Task-returning method +dotnet_diagnostic.VSTHRD114.severity = warning + +# Use Async suffix for async methods — silenced due to high violation count (218), naming convention churn +dotnet_diagnostic.VSTHRD200.severity = silent + dotnet_naming_style.underscored_camel_case.capitalization = camel_case dotnet_naming_style.underscored_camel_case.required_prefix = _ diff --git a/.github/labelChecker/.npmrc b/.github/labelChecker/.npmrc new file mode 100644 index 0000000000..615a78f4e4 --- /dev/null +++ b/.github/labelChecker/.npmrc @@ -0,0 +1,3 @@ +registry=https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/ + +always-auth=true diff --git a/.github/labelChecker/package-lock.json b/.github/labelChecker/package-lock.json index 09a058c1d7..c65084911b 100644 --- a/.github/labelChecker/package-lock.json +++ b/.github/labelChecker/package-lock.json @@ -1,139 +1,279 @@ { "name": "labelchecker", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@actions/core": { + "packages": { + "": { + "name": "labelchecker", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@actions/core": "1.2.6", + "@actions/github": "2.1.1", + "typed-rest-client": "2.3.0" + } + }, + "node_modules/@actions/core": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", - "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@actions/core/-/core-1.2.6.tgz", + "integrity": "sha1-p41J9BpN7xjojOR8LKxhXVaUvwk=", + "license": "MIT" }, - "@actions/github": { + "node_modules/@actions/github": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.1.1.tgz", - "integrity": "sha512-kAgTGUx7yf5KQCndVeHSwCNZuDBvPyxm5xKTswW2lofugeuC1AZX73nUUVDNaysnM9aKFMHv9YCdVJbg7syEyA==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@actions/github/-/github-2.1.1.tgz", + "integrity": "sha1-vKvt/1mBltlT9YunUNXnVUmnUUI=", + "license": "MIT", + "dependencies": { "@actions/http-client": "^1.0.3", "@octokit/graphql": "^4.3.1", "@octokit/rest": "^16.43.1" } }, - "@actions/http-client": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz", - "integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==", - "requires": { + "node_modules/@actions/http-client": { + "version": "1.0.11", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha1-xYsS6aqLFZ7jnn3Wy9DpHZBWM8A=", + "license": "MIT", + "dependencies": { "tunnel": "0.0.6" + } + }, + "node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha1-J8N+omwgXyhENAJHf/0mExHyHjY=", + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha1-DVhwQ5HGtoHewRFyQOpNKpisORY=", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha1-sC6cCKLYk33wmiqYHyJq0hkXTFM=", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha1-rPX3/t3eThIYXVMS7jj/dyNdggU=", + "license": "MIT", + "peer": true, "dependencies": { - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" - } + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, - "@octokit/auth-token": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.0.tgz", - "integrity": "sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg==", - "requires": { - "@octokit/types": "^2.0.0" + "node_modules/@octokit/core/node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha1-W4NBwiWQnpJLRmcFwTR3+s6GlFY=", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" } }, - "@octokit/endpoint": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.3.tgz", - "integrity": "sha512-EzKwkwcxeegYYah5ukEeAI/gYRLv2Y9U5PpIsseGSFDk+G3RbipQGBs8GuYS1TLCtQaqoO66+aQGtITPalxsNQ==", - "requires": { - "@octokit/types": "^2.0.0", - "is-plain-object": "^3.0.0", - "universal-user-agent": "^5.0.0" + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha1-N06lN4GWX9AqnTbKy5fhUs7/8S0=", + "license": "MIT", + "peer": true + }, + "node_modules/@octokit/core/node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha1-Zgmlo4rW+O4gPZ64rJNh2QakQU4=", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha1-RA+jyuMQRmiJd49aIitHpYB0Njg=", + "license": "MIT", + "peer": true, "dependencies": { - "universal-user-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz", - "integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==", - "requires": { - "os-name": "^3.1.0" - } - } + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" } }, - "@octokit/graphql": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz", - "integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==", - "requires": { - "@octokit/request": "^5.3.0", - "@octokit/types": "^2.0.0", - "universal-user-agent": "^4.0.0" + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha1-+9f6WQwu8ir4gbHXl1i/qiNNu3w=", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@octokit/core/node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha1-zxRHq5Fg32pA82Idpk1v/DYFDLk=", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@octokit/core/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha1-wFhwpYElotwAQx8t+BWnf+aXNr4=", + "license": "ISC", + "peer": true + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha1-O01HpLDnmxAn+4111CIZKLLQVlg=", + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" } }, - "@octokit/plugin-paginate-rest": { + "node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha1-Zk2bEcDhIRLL944Q9JoFlZqiLMM=", + "license": "MIT", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha1-2lY41k8rkZvKic5mAtBZ8bUtPvA=", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", - "integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", + "integrity": "sha1-AEFwrPjCvlNauiZyeGfWkve0iPw=", + "license": "MIT", + "dependencies": { "@octokit/types": "^2.0.1" } }, - "@octokit/plugin-request-log": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz", - "integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw==" + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha1-TF+No8b+zz2hgRrvZ4/aA+2sNdI=", + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha1-XlDtcIOmE4FrHkooruxft/FGLoU=", + "license": "MIT", + "peerDependencies": { + "@octokit/core": ">=3" + } }, - "@octokit/plugin-rest-endpoint-methods": { + "node_modules/@octokit/plugin-rest-endpoint-methods": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", - "integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", + "integrity": "sha1-Mojs9UgfaMSU3QYC/BVAeln69h4=", + "license": "MIT", + "dependencies": { "@octokit/types": "^2.0.1", "deprecation": "^2.3.1" } }, - "@octokit/request": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.2.tgz", - "integrity": "sha512-7NPJpg19wVQy1cs2xqXjjRq/RmtSomja/VSWnptfYwuBxLdbYh2UjhGi0Wx7B1v5Iw5GKhfFDQL7jM7SSp7K2g==", - "requires": { - "@octokit/endpoint": "^5.5.0", - "@octokit/request-error": "^1.0.1", - "@octokit/types": "^2.0.0", - "deprecation": "^2.0.0", - "is-plain-object": "^3.0.0", - "node-fetch": "^2.3.0", - "once": "^1.4.0", - "universal-user-agent": "^5.0.0" - }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha1-TF+No8b+zz2hgRrvZ4/aA+2sNdI=", + "license": "MIT", "dependencies": { - "universal-user-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz", - "integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==", - "requires": { - "os-name": "^3.1.0" - } - } + "@types/node": ">= 8" } }, - "@octokit/request-error": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz", - "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==", - "requires": { - "@octokit/types": "^2.0.0", + "node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha1-GaAiUVpbupZawGydEzRRTrUMSLA=", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha1-nhUDV4Mb/HiNE6T9SxkT1gx01nc=", + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", "deprecation": "^2.0.0", "once": "^1.4.0" } }, - "@octokit/rest": { - "version": "16.43.1", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz", - "integrity": "sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw==", - "requires": { + "node_modules/@octokit/rest": { + "version": "16.43.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/rest/-/rest-16.43.2.tgz", + "integrity": "sha1-xTQm8eHRBE3ulnAj4yecUJk92Rs=", + "license": "MIT", + "dependencies": { "@octokit/auth-token": "^2.4.0", "@octokit/plugin-paginate-rest": "^1.1.1", "@octokit/plugin-request-log": "^1.0.0", @@ -152,73 +292,191 @@ "universal-user-agent": "^4.0.0" } }, - "@octokit/types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.2.0.tgz", - "integrity": "sha512-iEeW3XlkxeM/CObeoYvbUv24Oe+DldGofY+3QyeJ5XKKA6B+V94ePk14EDCarseWdMs6afKZPv3dFq8C+SY5lw==", - "requires": { + "node_modules/@octokit/rest/node_modules/@octokit/request-error": { + "version": "1.2.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request-error/-/request-error-1.2.1.tgz", + "integrity": "sha1-7eBxTHc/MjR1dsJWSdwBOuazGAE=", + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha1-TF+No8b+zz2hgRrvZ4/aA+2sNdI=", + "license": "MIT", + "dependencies": { "@types/node": ">= 8" } }, - "@types/node": { - "version": "13.7.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.6.tgz", - "integrity": "sha512-eyK7MWD0R1HqVTp+PtwRgFeIsemzuj4gBFSQxfPHY5iMjS7474e5wq+VFgTcdpyHeNxyKSaetYAjdMLJlKoWqA==" + "node_modules/@octokit/rest/node_modules/universal-user-agent": { + "version": "4.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/universal-user-agent/-/universal-user-agent-4.0.1.tgz", + "integrity": "sha1-/Y1st3OmeacJ6WfvgoijH8wD5Vc=", + "license": "ISC", + "dependencies": { + "os-name": "^3.1.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha1-5Y73jXhZbS+335xiWYAkZLX4SgQ=", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@types/node/-/node-25.5.0.tgz", + "integrity": "sha1-XJnzfEQ9nMxJhYZpE/HtNkIX2jE=", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } }, - "atob-lite": { + "node_modules/atob-lite": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/atob-lite/-/atob-lite-2.0.0.tgz", + "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=", + "license": "MIT" }, - "before-after-hook": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz", - "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==" + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha1-xR6AnIGk41QIRCK5smutiCScUXw=", + "license": "Apache-2.0" }, - "btoa-lite": { + "node_modules/btoa-lite": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", + "license": "MIT" }, - "call-bind": { + "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha1-S1QowiK+mF15w9gmV0edvgtZstY=", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha1-I43pNdKippKSjFOMfM+pEGf9Bio=", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha1-MNDvoHEt2361p24ehyG/+vprXVc=", + "license": "MIT", + "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" } }, - "deprecation": { + "node_modules/deprecation": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha1-Y2jL20Cr8zc7UlrIfkomDDpwCRk=", + "license": "ISC" + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha1-HTf1dm87v/Tuljjocah2jBc7gdo=", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha1-165mfh3INIL4tw/Q9u78UNow9Yo=", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha1-c0TXEd6kDgt0q8LtSXeHQ8ztsIw=", + "license": "MIT", + "dependencies": { "once": "^1.4.0" } }, - "execa": { + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha1-mD6y+aZyTpMD9hrd8BHHLgngsPo=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha1-BfdaJdq5jk+x3NXhRywFRtUFfI8=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha1-HE8sSDcydZfOadLKGQp/3RcjOME=", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/execa": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/execa/-/execa-1.0.0.tgz", + "integrity": "sha1-xiNqW7TfbW8V6I5/AXeYIWdJ3dg=", + "license": "MIT", + "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", @@ -226,267 +484,545 @@ "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha1-VZC2yAfMWYvhJeZ0Cp/eWJ0revs=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "peer": true }, - "get-intrinsic": { + "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha1-LALYZNl/PqbIgwxGTL0Rq26rehw=", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "get-stream": { + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha1-dD8OO2lkqTpUke0b/6rgVNf5jQE=", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha1-FQs/J0OGnvPoUewMSdFbHRTQDuE=", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha1-wbJVV189wh1Zv8ec09K0axw6VLU=", + "license": "MIT", + "dependencies": { "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha1-ifVrghe9vIgCvSmd9tfxCB1+UaE=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha1-/JxqeDoISVHQuXH+EBjegTcHozg=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "is-plain-object": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", - "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", - "requires": { - "isobject": "^4.0.0" + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha1-AD6vkb563DcuhOxZ3DclLO24AAM=", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=", + "license": "ISC" + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha1-RCf1CrNCnpAl6n1S6QQ6nvQVk0Q=", + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "is-stream": { + "node_modules/is-stream": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "isexe": { + "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "license": "ISC" }, - "isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==" + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha1-zTs9wEWwxARVbIHdtXVsI+WdfPU=", + "license": "MIT" }, - "lodash.get": { + "node_modules/json-with-bigint": { + "version": "3.5.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha1-Gx7bVaG8SBbKh6xoQpdZGs2CI4M=", + "license": "MIT", + "peer": true + }, + "node_modules/lodash.get": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "license": "MIT" }, - "lodash.set": { + "node_modules/lodash.set": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "license": "MIT" }, - "lodash.uniq": { + "node_modules/lodash.uniq": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "license": "MIT" }, - "macos-release": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", - "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==" + "node_modules/macos-release": { + "version": "2.5.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha1-vMrEqPe5MWOo0WO46/OFs8X1W/k=", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "nice-try": { + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha1-oN10voHiqlwvJ+Zc4oNgXuTit/k=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha1-LhlN4ERibUoQ5/f7wAznPoPk1cc=", + "license": "ISC" + }, + "node_modules/nice-try": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha1-ozeKdpbOfSI+iPybdkvX7xCJ42Y=", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha1-0PD6bj4twdJ+/NitmdVQvalNGH0=", + "license": "MIT", + "dependencies": { "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "npm-run-path": { + "node_modules/npm-run-path": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { + "license": "MIT", + "dependencies": { "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" } }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha1-g3UmXiG8IND6WCwi4bE0hdbgAhM=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "octokit-pagination-methods": { + "node_modules/octokit-pagination-methods": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", + "integrity": "sha1-z0cu3J1VEFX573P25CtNu0yAvqQ=", + "license": "MIT" }, - "once": { + "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { + "license": "ISC", + "dependencies": { "wrappy": "1" } }, - "os-name": { + "node_modules/os-name": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha1-3sGdlmKW4c1i1wGlpm7h3ernCAE=", + "license": "MIT", + "dependencies": { "macos-release": "^2.2.0", "windows-release": "^3.1.0" + }, + "engines": { + "node": ">=6" } }, - "p-finally": { + "node_modules/p-finally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "path-key": { + "node_modules/path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/pump/-/pump-3.0.4.tgz", + "integrity": "sha1-HzE0MFJ/qLkFYi69Iv4UROdXqzw=", + "license": "MIT", + "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/qs/-/qs-6.15.0.tgz", + "integrity": "sha1-24/V0bHS1rWzOtr4dCmAXxkJ57M=", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/semver/-/semver-5.7.2.tgz", + "integrity": "sha1-SNVdtzfDKHzUg14X+hP+rOHEHvg=", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } }, - "shebang-command": { + "node_modules/shebang-command": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { + "license": "MIT", + "dependencies": { "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "shebang-regex": { + "node_modules/shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha1-w/z/nE2pMnhIczNeyXZfqU/2a8k=", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha1-EMtZhCYxFdO3oOM2WR4pCoMK+K0=", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha1-1rtrN5Asb+9RdOX1M/q0xzKib0I=", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha1-Ed2hnVNo5Azp7CvcH7DsvAeQ7Oo=", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha1-qaF2f4r4QVURTqq9c/mSc8j1mtk=", + "license": "ISC" }, - "strip-eof": { + "node_modules/strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "tr46": { + "node_modules/tr46": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "license": "MIT" }, - "tunnel": { + "node_modules/tunnel": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" - }, - "typed-rest-client": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.9.tgz", - "integrity": "sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g==", - "requires": { - "qs": "^6.9.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha1-cvExSzSlsZLbASMk3yzFh8pH+Sw=", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typed-rest-client": { + "version": "2.3.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/typed-rest-client/-/typed-rest-client-2.3.0.tgz", + "integrity": "sha1-YBK4gubNvRsOC2BAf31OP6VJih0=", + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "^6.14.1", "tunnel": "0.0.6", - "underscore": "^1.12.1" + "underscore": "^1.13.8" + }, + "engines": { + "node": ">= 16.0.0" } }, - "underscore": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", - "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==" + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha1-qTohGGwEnb8OhHSW26cre9jB6Ss=", + "license": "MIT" }, - "universal-user-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz", - "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==", - "requires": { - "os-name": "^3.1.0" - } + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha1-KTV6iee3ykrvO/D9P9DNc4hCKek=", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha1-FfIPVdo8kwxXvdvxc0xmVNX9Nao=", + "license": "ISC" }, - "webidl-conversions": { + "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==" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "license": "BSD-2-Clause" }, - "whatwg-url": { + "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==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "license": "MIT", + "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, - "which": { + "node_modules/which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/which/-/which-1.3.1.tgz", + "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", + "license": "ISC", + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "windows-release": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", - "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==", - "requires": { + "node_modules/windows-release": { + "version": "3.3.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha1-HBACfHIldD7sa4nfFg1kwuApOZk=", + "license": "MIT", + "dependencies": { "execa": "^1.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "license": "ISC" } } } diff --git a/.github/labelChecker/package.json b/.github/labelChecker/package.json index 55245ffcc4..68dc5366e2 100644 --- a/.github/labelChecker/package.json +++ b/.github/labelChecker/package.json @@ -11,6 +11,6 @@ "dependencies": { "@actions/core": "1.2.6", "@actions/github": "2.1.1", - "typed-rest-client": "^1.8.9" + "typed-rest-client": "2.3.0" } } diff --git a/.vsts.ci.yml b/.vsts.ci.yml index 7d2bcb3bb3..fc146f0f9f 100644 --- a/.vsts.ci.yml +++ b/.vsts.ci.yml @@ -61,6 +61,7 @@ extends: publishArtifacts: ${{ ne(variables['Build.Reason'], 'PullRequest') }} buildAlternatePackage: false testProxyAgent: ${{ parameters.testProxyAgent }} + targetFramework: 'all' win_x64: ${{ parameters.win_x64 }} win_x86: ${{ parameters.win_x86 }} win_arm64: ${{ parameters.win_arm64 }} diff --git a/.vsts.release.yml b/.vsts.release.yml index 6d427bdfee..c3a69ad365 100644 --- a/.vsts.release.yml +++ b/.vsts.release.yml @@ -30,6 +30,7 @@ parameters: default: net8.0 values: - net8.0 + - net10.0 - name: derivedFrom type: string @@ -92,7 +93,7 @@ extends: displayName: Verify it's a release run pool: name: 1ES-ABTT-Shared-Pool - image: abtt-ubuntu-2204 + image: abtt-ubuntu-2404 os: linux jobs: - job: Set_variables @@ -145,9 +146,9 @@ extends: ## Verify target framework for specified version $majorVersion = $agentVersion.Split('.')[0] - if (("${{ parameters.targetFramework }}" -eq "net6.0" -and $majorVersion -ne "3") -or - ("${{ parameters.targetFramework }}" -eq "net8.0" -and $majorVersion -ne "4")) { - Write-Error "The major version should be 3 for net6.0 and 4 for net8.0" -ErrorAction Stop + if (("${{ parameters.targetFramework }}" -eq "net8.0" -and $majorVersion -ne "4") -or + ("${{ parameters.targetFramework }}" -eq "net10.0" -and $majorVersion -ne "5")) { + Write-Error "The major version should be 4 for net8.0 and 5 for net10.0" -ErrorAction Stop } } if ($isTestRun) { @@ -182,16 +183,16 @@ extends: pool: name: 1ES-ABTT-Shared-Pool - image: abtt-ubuntu-2204 + image: abtt-ubuntu-2404 os: linux steps: - checkout: self - - task: NodeTool@0 - displayName: Use node 20.19.4 + - task: UseNode@1 + displayName: Use node 20.20.2 inputs: - versionSpec: "20.19.4" + version: "20.20.2" - script: | cd release @@ -214,7 +215,9 @@ extends: ################################################################################ displayName: Publish Agents (Windows/Linux/OSX) pool: - name: 1ES-Shared-Hosted-Pool_Windows-Server-2022 + name: 1ES-ABTT-Shared-Pool + image: abtt-windows-2025 + os: windows demands: AzurePS variables: IsTestRun: $[ stageDependencies.Verify_release.Set_variables.outputs['SetReleaseVariables.isTestRun'] ] @@ -384,7 +387,7 @@ extends: pool: name: 1ES-ABTT-Shared-Pool - image: abtt-ubuntu-2204 + image: abtt-ubuntu-2404 os: linux steps: - checkout: self @@ -413,7 +416,7 @@ extends: condition: and(succeeded(), not(${{ parameters.onlyGitHubRelease }})) pool: name: 1ES-ABTT-Shared-Pool - image: abtt-mariner + image: abtt-ubuntu-2404 os: linux jobs: ################################################################################ @@ -462,7 +465,7 @@ extends: - CreatePRs pool: name: 1ES-ABTT-Shared-Pool - image: abtt-ubuntu-2204 + image: abtt-ubuntu-2404 os: linux jobs: - job: j_SendPRsNotifications diff --git a/docs/node6.md b/docs/node6.md index 12ec74eac1..8246f27c67 100644 --- a/docs/node6.md +++ b/docs/node6.md @@ -8,7 +8,7 @@ As Node versions exit out of the upstream maintenance window, some Pipelines tas To accommodate this, we have 2 flavors of packages: -| Packages | Node versions | Description | -|----------------------|---------------|----------------------------| -| `vsts-agent-*` | 6, 10, 16, 20 | Includes all Node versions that can be used as task execution handler | -| `pipelines-agents-*` | 16, 20 | Includes only recent Node versions. The goal for these packages is to not include any end-of-life version of Node. | +| Packages | Node versions | Description | +|----------------------|------------------|----------------------------| +| `vsts-agent-*` | 6, 10, 16, 20, 24 | Includes all Node versions that can be used as task execution handler | +| `pipelines-agents-*` | 16, 20, 24 | Includes only recent Node versions. The goal for these packages is to not include any end-of-life version of Node. | diff --git a/docs/noderunner.md b/docs/noderunner.md index a9ff6682de..91dd864146 100644 --- a/docs/noderunner.md +++ b/docs/noderunner.md @@ -1,6 +1,6 @@ # Node 6 support -Agent tasks can be implemented in PowerShell or Node. The agent currently ships with three versions of Node that tasks can target: 6, 10 & 16. +Agent tasks can be implemented in PowerShell or Node. The agent currently ships with multiple versions of Node that tasks can target: 6, 10, 16, 20 & 24. Since Node 6 has long passed out of the upstream maintenance window, and all officially supported tasks are migrated from Node 6 to Node 10, Node 6 soon will be removed from the agent package. It's also highly recommended to third-party task maintainers migrate tasks to Node 10 or Node 16. diff --git a/release/.npmrc b/release/.npmrc new file mode 100644 index 0000000000..969ccea076 --- /dev/null +++ b/release/.npmrc @@ -0,0 +1,3 @@ +registry=https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/ + +always-auth=true \ No newline at end of file diff --git a/release/createReleaseBranch.js b/release/createReleaseBranch.js index f709a373a2..472725c242 100644 --- a/release/createReleaseBranch.js +++ b/release/createReleaseBranch.js @@ -139,10 +139,24 @@ async function fetchPRsSincePreviousReleaseAndEditReleaseNotes(newRelease, callb var filteredReleases = latestReleases.data.filter(release => !release.draft); // consider only pre-releases and published releases - var releaseTagPrefix = 'v' + newRelease.split('.')[0]; + var majorVersion = parseInt(newRelease.split('.')[0]); + var releaseTagPrefix = 'v' + majorVersion; console.log(`Getting latest release starting with ${releaseTagPrefix}`); var latestReleaseInfo = filteredReleases.find(release => release.tag_name.toLowerCase().startsWith(releaseTagPrefix.toLowerCase())); + + // Fall back to previous major version if no releases found for current major version + if (!latestReleaseInfo && majorVersion > 0) { + var fallbackPrefix = 'v' + (majorVersion - 1); + console.log(`No releases found with prefix ${releaseTagPrefix}, falling back to ${fallbackPrefix}`); + latestReleaseInfo = filteredReleases.find(release => release.tag_name.toLowerCase().startsWith(fallbackPrefix.toLowerCase())); + } + + if (!latestReleaseInfo) { + console.log(`Error: No releases found with prefix ${releaseTagPrefix} or fallback. Aborting.`); + process.exit(-1); + } + console.log(`Previous release tag with ${latestReleaseInfo.tag_name} and published date is: ${latestReleaseInfo.published_at}`) try { diff --git a/release/package-lock.json b/release/package-lock.json index 33f3d52bba..ed45598fb9 100644 --- a/release/package-lock.json +++ b/release/package-lock.json @@ -1,6 +1,6 @@ { "name": "release", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -8,175 +8,212 @@ "@octokit/graphql": "^7.1.1", "@octokit/rest": "^16.43.2", "azure-devops-node-api": "^12.0.0", - "azure-pipelines-task-lib": "^4.3.1", + "azure-pipelines-task-lib": "^5.2.8", "got": "^11.8.6", "node-getopt": "^0.3.2" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha1-dhnC6yGyVIP20WdUi0z9WnSIw9U=", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha1-W9Jir5Tp0lvR5xsF3u1Eh2oiLos=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha1-6Vc36LtnRt3t9pxVaVNJTxlv5po=", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@octokit/auth-token": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", - "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha1-J8N+omwgXyhENAJHf/0mExHyHjY=", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^6.0.3" } }, + "node_modules/@octokit/auth-token/node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha1-2lY41k8rkZvKic5mAtBZ8bUtPvA=", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-token/node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha1-5Y73jXhZbS+335xiWYAkZLX4SgQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, "node_modules/@octokit/core": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", - "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", + "version": "7.0.6", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha1-DVhwQ5HGtoHewRFyQOpNKpisORY=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.2.2", - "@octokit/request": "^9.2.3", - "@octokit/request-error": "^6.1.8", - "@octokit/types": "^14.0.0", - "before-after-hook": "^3.0.2", + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/auth-token": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", - "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", + "version": "6.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha1-sC6cCKLYk33wmiqYHyJq0hkXTFM=", "dev": true, + "license": "MIT", "peer": true, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/endpoint": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", - "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "version": "11.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha1-rPX3/t3eThIYXVMS7jj/dyNdggU=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^14.0.0", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/graphql": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", - "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "version": "9.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha1-W4NBwiWQnpJLRmcFwTR3+s6GlFY=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@octokit/request": "^9.2.3", - "@octokit/types": "^14.0.0", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "version": "27.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha1-N06lN4GWX9AqnTbKy5fhUs7/8S0=", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@octokit/core/node_modules/@octokit/request": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", - "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", + "version": "10.0.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha1-Zgmlo4rW+O4gPZ64rJNh2QakQU4=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@octokit/endpoint": "^10.1.4", - "@octokit/request-error": "^6.1.8", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^2.0.0", + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/request-error": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", - "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "version": "7.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha1-RA+jyuMQRmiJd49aIitHpYB0Njg=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^14.0.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", - "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "version": "16.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha1-+9f6WQwu8ir4gbHXl1i/qiNNu3w=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@octokit/openapi-types": "^25.0.0" + "@octokit/openapi-types": "^27.0.0" } }, "node_modules/@octokit/core/node_modules/before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "version": "4.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha1-zxRHq5Fg32pA82Idpk1v/DYFDLk=", "dev": true, + "license": "Apache-2.0", "peer": true }, "node_modules/@octokit/core/node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", + "version": "7.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha1-wFhwpYElotwAQx8t+BWnf+aXNr4=", "dev": true, + "license": "ISC", "peer": true }, "node_modules/@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", - "dev": true, - "dependencies": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/endpoint/node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true - }, - "node_modules/@octokit/graphql": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", - "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", - "dev": true, - "dependencies": { - "@octokit/request": "^8.4.1", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", - "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha1-EU2RIQj+aS2LE5z+f8CEbf0RtsA=", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" @@ -185,94 +222,64 @@ "node": ">= 18" } }, - "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true - }, - "node_modules/@octokit/graphql/node_modules/@octokit/request": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", - "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha1-ednz0Mlqj9E9ZBhv5cM2BtSLecw=", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^9.0.6", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.1.0", + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", - "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", - "dev": true, - "dependencies": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, - "dependencies": { - "@octokit/openapi-types": "^24.2.0" - } - }, - "node_modules/@octokit/graphql/node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", - "dev": true - }, "node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", - "dev": true + "version": "24.2.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha1-PVXDLqwNONoacIOpw7DMp3kk99M=", + "dev": true, + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", - "integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", + "integrity": "sha1-AEFwrPjCvlNauiZyeGfWkve0iPw=", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^2.0.1" } }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", - "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha1-TF+No8b+zz2hgRrvZ4/aA+2sNdI=", "dev": true, + "license": "MIT", "dependencies": { "@types/node": ">= 8" } }, "node_modules/@octokit/plugin-request-log": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha1-XlDtcIOmE4FrHkooruxft/FGLoU=", "dev": true, + "license": "MIT", "peerDependencies": { "@octokit/core": ">=3" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", - "integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", + "integrity": "sha1-Mojs9UgfaMSU3QYC/BVAeln69h4=", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^2.0.1", "deprecation": "^2.3.1" @@ -280,18 +287,102 @@ }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", - "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha1-TF+No8b+zz2hgRrvZ4/aA+2sNdI=", "dev": true, + "license": "MIT", "dependencies": { "@types/node": ">= 8" } }, "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha1-cVoBXM+ZMIeXfqQ2XER5H8RXJIY=", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha1-uSGPnBFm5ou00MibY47cYskzSAU=", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "16.43.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/rest/-/rest-16.43.2.tgz", + "integrity": "sha1-xTQm8eHRBE3ulnAj4yecUJk92Rs=", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^2.4.0", + "@octokit/plugin-paginate-rest": "^1.1.1", + "@octokit/plugin-request-log": "^1.0.0", + "@octokit/plugin-rest-endpoint-methods": "2.4.0", + "@octokit/request": "^5.2.0", + "@octokit/request-error": "^1.0.2", + "atob-lite": "^2.0.0", + "before-after-hook": "^2.0.0", + "btoa-lite": "^1.0.0", + "deprecation": "^2.0.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", + "lodash.uniq": "^4.5.0", + "octokit-pagination-methods": "^1.1.0", + "once": "^1.4.0", + "universal-user-agent": "^4.0.0" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha1-O01HpLDnmxAn+4111CIZKLLQVlg=", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/endpoint/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha1-FfIPVdo8kwxXvdvxc0xmVNX9Nao=", + "dev": true, + "license": "ISC" + }, + "node_modules/@octokit/rest/node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha1-2lY41k8rkZvKic5mAtBZ8bUtPvA=", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/rest/node_modules/@octokit/request": { "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", - "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha1-GaAiUVpbupZawGydEzRRTrUMSLA=", "dev": true, + "license": "MIT", "dependencies": { "@octokit/endpoint": "^6.0.1", "@octokit/request-error": "^2.1.0", @@ -301,81 +392,83 @@ "universal-user-agent": "^6.0.0" } }, - "node_modules/@octokit/request-error": { + "node_modules/@octokit/rest/node_modules/@octokit/request-error": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz", - "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request-error/-/request-error-1.2.1.tgz", + "integrity": "sha1-7eBxTHc/MjR1dsJWSdwBOuazGAE=", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^2.0.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, - "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "node_modules/@octokit/rest/node_modules/@octokit/request-error/node_modules/@octokit/types": { "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", - "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha1-TF+No8b+zz2hgRrvZ4/aA+2sNdI=", "dev": true, + "license": "MIT", "dependencies": { "@types/node": ">= 8" } }, - "node_modules/@octokit/request/node_modules/@octokit/request-error": { + "node_modules/@octokit/rest/node_modules/@octokit/request/node_modules/@octokit/request-error": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha1-nhUDV4Mb/HiNE6T9SxkT1gx01nc=", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^6.0.3", "deprecation": "^2.0.0", "once": "^1.4.0" } }, - "node_modules/@octokit/request/node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true + "node_modules/@octokit/rest/node_modules/@octokit/request/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha1-FfIPVdo8kwxXvdvxc0xmVNX9Nao=", + "dev": true, + "license": "ISC" }, - "node_modules/@octokit/rest": { - "version": "16.43.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.2.tgz", - "integrity": "sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==", + "node_modules/@octokit/rest/node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha1-5Y73jXhZbS+335xiWYAkZLX4SgQ=", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/auth-token": "^2.4.0", - "@octokit/plugin-paginate-rest": "^1.1.1", - "@octokit/plugin-request-log": "^1.0.0", - "@octokit/plugin-rest-endpoint-methods": "2.4.0", - "@octokit/request": "^5.2.0", - "@octokit/request-error": "^1.0.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^2.0.0", - "btoa-lite": "^1.0.0", - "deprecation": "^2.0.0", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" + "@octokit/openapi-types": "^12.11.0" + } + }, + "node_modules/@octokit/rest/node_modules/universal-user-agent": { + "version": "4.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/universal-user-agent/-/universal-user-agent-4.0.1.tgz", + "integrity": "sha1-/Y1st3OmeacJ6WfvgoijH8wD5Vc=", + "dev": true, + "license": "ISC", + "dependencies": { + "os-name": "^3.1.0" } }, "node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "version": "13.10.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha1-PnxrGcAjbCcGVuTqZmFIwrUf0aM=", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^12.11.0" + "@octokit/openapi-types": "^24.2.0" } }, "node_modules/@sindresorhus/is": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha1-PHycRuZ4/u/nouW7YJ09vWZf+z8=", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -385,9 +478,10 @@ }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha1-tKkUu2LnwnLU5Zif5EQPgSqx2Ac=", "dev": true, + "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" }, @@ -397,9 +491,10 @@ }, "node_modules/@types/cacheable-request": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha1-pDCzJgRmyntcpb/XNWk7Nuep0YM=", "dev": true, + "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", @@ -408,39 +503,46 @@ } }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", - "dev": true + "version": "4.2.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha1-9qd4j0OMv94V8prK1GUStMAZE7M=", + "dev": true, + "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha1-PM2xxnUbDH5SMAvNrNW8v4+qdbY=", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz", - "integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==", - "dev": true + "version": "25.5.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@types/node/-/node-25.5.2.tgz", + "integrity": "sha1-lIYeMvn/2N4QtSu+xANGXIT/92I=", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } }, "node_modules/@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "version": "1.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha1-zClwbwo5fP5t+J3r/kv1zqFZ21A=", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "version": "0.5.17", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha1-XAtl83ruxcKpSZXAJPkx9i5LvFo=", "dev": true, "license": "MIT", "engines": { @@ -449,8 +551,8 @@ }, "node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha1-Sf/1hXfP7j83F2/qtMIuAPhtf3c=", "dev": true, "license": "MIT", "dependencies": { @@ -462,52 +564,56 @@ }, "node_modules/atob-lite": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/atob-lite/-/atob-lite-2.0.0.tgz", + "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=", + "dev": true, + "license": "MIT" }, "node_modules/azure-devops-node-api": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.0.0.tgz", - "integrity": "sha512-S6Il++7dQeMlZDokBDWw7YVoPeb90tWF10pYxnoauRMnkuL91jq9M7SOYRVhtO3FUC5URPkB/qzGa7jTLft0Xw==", + "version": "12.5.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha1-OLnv18WsdDVP5Ojb5CaX2wuOhaU=", "dev": true, + "license": "MIT", "dependencies": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, "node_modules/azure-pipelines-task-lib": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-4.17.3.tgz", - "integrity": "sha512-UxfH5pk3uOHTi9TtLtdDyugQVkFES5A836ZEePjcs3jYyxm3EJ6IlFYq6gbfd6mNBhrM9fxG2u/MFYIJ+Z0cxQ==", + "version": "5.2.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/azure-pipelines-task-lib/-/azure-pipelines-task-lib-5.2.8.tgz", + "integrity": "sha1-GTBOm8FZy35Zx6kShvq/0qgqPFA=", "dev": true, "license": "MIT", "dependencies": { "adm-zip": "^0.5.10", - "minimatch": "3.0.5", + "minimatch": "^3.1.5", "nodejs-file-downloader": "^4.11.1", "q": "^1.5.1", "semver": "^5.7.2", - "shelljs": "^0.8.5", + "shelljs": "^0.10.0", "uuid": "^3.0.1" } }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", + "dev": true, + "license": "MIT" }, "node_modules/before-after-hook": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha1-xR6AnIGk41QIRCK5smutiCScUXw=", + "dev": true, + "license": "Apache-2.0" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha1-03h1wB3J7/mI3UnREqV8tntU7+Y=", "dev": true, "license": "MIT", "dependencies": { @@ -515,26 +621,42 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/braces/-/braces-3.0.3.tgz", + "integrity": "sha1-SQMy9AkZRSJy1VqEgK3AxEE1h4k=", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/btoa-lite": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", + "dev": true, + "license": "MIT" }, "node_modules/cacheable-lookup": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha1-WmuGWyxENXvj1evCpGewMnGacAU=", "dev": true, + "license": "MIT", "engines": { "node": ">=10.6.0" } }, "node_modules/cacheable-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", - "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "version": "7.0.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha1-ejPr8IYTF4tANjW+e4mdPmm76Bc=", "dev": true, + "license": "MIT", "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -548,29 +670,32 @@ "node": ">=8" } }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha1-S1QowiK+mF15w9gmV0edvgtZstY=", "dev": true, + "license": "MIT", "dependencies": { - "pump": "^3.0.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha1-I43pNdKippKSjFOMfM+pEGf9Bio=", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -578,9 +703,10 @@ }, "node_modules/clone-response": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha1-ryAyqkeBY5nPXwodDbkC9ReruMM=", "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" }, @@ -590,31 +716,30 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "version": "7.0.6", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8=", "dev": true, "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" + "node": ">= 8" } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/debug/-/debug-4.4.3.tgz", + "integrity": "sha1-xq5DLZvZZiWC/OCHCbA4xY6ePWo=", "dev": true, "license": "MIT", "dependencies": { @@ -631,9 +756,10 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha1-yjh2Et234QS9FthaqwDV7PCcZvw=", "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -646,9 +772,10 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha1-LR1Zr5wbEpgVrMwsRqAipc4fo8k=", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -658,50 +785,120 @@ }, "node_modules/defer-to-connect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha1-gBa9tBQ+RjK3ejRJxiNid95SBYc=", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/deprecation": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha1-Y2jL20Cr8zc7UlrIfkomDDpwCRk=", + "dev": true, + "license": "ISC" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha1-165mfh3INIL4tw/Q9u78UNow9Yo=", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha1-c0TXEd6kDgt0q8LtSXeHQ8ztsIw=", "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha1-mD6y+aZyTpMD9hrd8BHHLgngsPo=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha1-BfdaJdq5jk+x3NXhRywFRtUFfI8=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha1-HE8sSDcydZfOadLKGQp/3RcjOME=", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "version": "5.1.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/execa/-/execa-5.1.1.tgz", + "integrity": "sha1-+ArZy/Qpj3vR1MlVXCHpN0HEEd0=", "dev": true, + "license": "MIT", "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha1-omLY7vZ6ztV8KFKtYWdSakPL97c=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/fast-content-type-parse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", - "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "version": "3.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha1-VZC2yAfMWYvhJeZ0Cp/eWJ0revs=", "dev": true, "funding": [ { @@ -713,12 +910,53 @@ "url": "https://opencollective.com/fastify" } ], + "license": "MIT", "peer": true }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha1-0G1YXOjbqQoWsFBcVDw8z7OuuBg=", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha1-ynUKENySW8ixiDn9ID4+9LPO1nU=", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha1-RCZdPKwH4+p9wkdRY4BkN1SgUpI=", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha1-d31z1yqS+OxNLkEOtHNSpWuOg0A=", "dev": true, "funding": [ { @@ -736,83 +974,103 @@ } } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha1-LALYZNl/PqbIgwxGTL0Rq26rehw=", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.3.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha1-dD8OO2lkqTpUke0b/6rgVNf5jQE=", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha1-FQs/J0OGnvPoUewMSdFbHRTQDuE=", "dev": true, + "license": "MIT", "dependencies": { - "pump": "^3.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha1-SWaheV7lrOZecGxLe+txJX1uItM=", "dev": true, + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "pump": "^3.0.0" }, "engines": { - "node": "*" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha1-hpgyxYA0/mikCTwX3BXoNA2EAcQ=", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "is-glob": "^4.0.1" }, "engines": { - "node": "*" + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha1-ifVrghe9vIgCvSmd9tfxCB1+UaE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/got": { "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/got/-/got-11.8.6.tgz", + "integrity": "sha1-J26Cfq2Hcu3bz8lxcFkLhBgjIzo=", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -833,23 +1091,12 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha1-/JxqeDoISVHQuXH+EBjegTcHozg=", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -857,29 +1104,32 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha1-AD6vkb563DcuhOxZ3DclLO24AAM=", "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true + "version": "4.2.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha1-IF9Ntk+FYrdqT/kjWqUnmDmgndU=", + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/http2-wrapper": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha1-uPVeDB8l1OvQizsMLAeflZCACz0=", "dev": true, + "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" @@ -890,8 +1140,8 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha1-xZ7yJKBP6LdU89sAY6Jeow0ABdY=", "dev": true, "license": "MIT", "dependencies": { @@ -902,116 +1152,141 @@ "node": ">= 6" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha1-3JH8ukLk0G5Kuu0zs+ejwC9RTqA=", "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=", "dev": true, + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "is-extglob": "^2.1.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" } }, "node_modules/is-plain-object": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha1-RCf1CrNCnpAl6n1S6QQ6nvQVk0Q=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "version": "2.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha1-+sHj1TuXrVqdCunO8jifWBClwHc=", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true, + "license": "ISC" }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=", + "dev": true, + "license": "MIT" + }, + "node_modules/json-with-bigint": { + "version": "3.5.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha1-Gx7bVaG8SBbKh6xoQpdZGs2CI4M=", + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "version": "4.5.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha1-qHmpnilFL5QkOfKkBeOvizHU3pM=", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/lodash.get": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true, + "license": "MIT" }, "node_modules/lodash.set": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true, + "license": "MIT" }, "node_modules/lodash.uniq": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true, + "license": "MIT" }, "node_modules/lowercase-keys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha1-JgPni3tLAAbLyi+8yKMgJVislHk=", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/macos-release": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", - "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha1-vMrEqPe5MWOo0WO46/OFs8X1W/k=", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -1019,10 +1294,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha1-oN10voHiqlwvJ+Zc4oNgXuTit/k=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha1-UoI2KaFN0AyXcPtq1H3GMQ8sH2A=", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha1-Q2iJL4hekHRVpv19xVwMnUBJkK4=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha1-1m+hjzpHB2eJMgubGvMr2G2fogI=", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha1-u6vNwChZ9JhzAchW4zh85exDv3A=", "dev": true, "license": "MIT", "engines": { @@ -1031,8 +1347,8 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha1-OBqHG2KnNEUGYK497uRIE/cNlZo=", "dev": true, "license": "MIT", "dependencies": { @@ -1042,20 +1358,32 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha1-ftLCzMyvhNP/y3pptXcR/CCDQBs=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha1-SSNTiHju9CBjy4o+OweYeBSHqxs=", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "version": "3.1.5", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1065,22 +1393,24 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/ms/-/ms-2.1.3.tgz", + "integrity": "sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=", "dev": true, "license": "MIT" }, "node_modules/nice-try": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha1-ozeKdpbOfSI+iPybdkvX7xCJ42Y=", + "dev": true, + "license": "MIT" }, "node_modules/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "version": "2.7.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha1-0PD6bj4twdJ+/NitmdVQvalNGH0=", "dev": true, + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -1098,17 +1428,18 @@ }, "node_modules/node-getopt": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/node-getopt/-/node-getopt-0.3.2.tgz", - "integrity": "sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/node-getopt/-/node-getopt-0.3.2.tgz", + "integrity": "sha1-V1B80i9vaWUKqZJSMEqELxIk5Ew=", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } }, "node_modules/nodejs-file-downloader": { "version": "4.13.0", - "resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz", - "integrity": "sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz", + "integrity": "sha1-2ofDAIHeX/TouGQGLJjN7APmatA=", "dev": true, "license": "ISC", "dependencies": { @@ -1120,9 +1451,10 @@ }, "node_modules/normalize-url": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha1-QNCIW1Nd7/4/MUe+yHfQX+TFZoo=", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1131,46 +1463,70 @@ } }, "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "version": "4.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha1-t+zR5e1T2o43pV4cImnguX7XSOo=", "dev": true, + "license": "MIT", "dependencies": { - "path-key": "^2.0.0" + "path-key": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha1-g3UmXiG8IND6WCwi4bE0hdbgAhM=", "dev": true, - "funding": { + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/octokit-pagination-methods": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", + "integrity": "sha1-z0cu3J1VEFX573P25CtNu0yAvqQ=", + "dev": true, + "license": "MIT" }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha1-0Oluu1awdHbfHdnEgG5SN5hcpF4=", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/os-name": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha1-3sGdlmKW4c1i1wGlpm7h3ernCAE=", "dev": true, + "license": "MIT", "dependencies": { "macos-release": "^2.2.0", "windows-release": "^3.1.0" @@ -1181,51 +1537,53 @@ }, "node_modules/p-cancelable": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha1-qrf71BZYL6MqPbSYWcEiSHxe0s8=", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/p-finally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha1-WpQpFeJrNy3A8OZ1MUmhbmscVgE=", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/pump/-/pump-3.0.4.tgz", + "integrity": "sha1-HzE0MFJ/qLkFYi69Iv4UROdXqzw=", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -1233,22 +1591,24 @@ }, "node_modules/q": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.0", "teleport": ">=0.2.0" } }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.15.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/qs/-/qs-6.15.1.tgz", + "integrity": "sha1-vbVa7Qa/rCV6kMRKRGpz+6VXXI8=", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -1257,11 +1617,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha1-SSkii7xyTfrEPg77BYyve2z7YkM=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/quick-lru": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha1-NmST5rPkKjpoheLpnRj4D7eoyTI=", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1269,46 +1651,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dev": true, - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-alpn": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha1-t629rDVGqq7CC0Xn2CZZJwcnJvk=", + "dev": true, + "license": "MIT" }, "node_modules/responselike": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha1-mgvI/cJS8/scymiwFlkQWboUIrw=", "dev": true, + "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" }, @@ -1316,10 +1671,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha1-D+E7lSLhRz9RtVjueW4I8R+bSJ8=", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha1-ZtE2jae9+SHrnZW9GpIp5/IaQ+4=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "version": "1.6.4", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha1-trOevtm9GhiYuFxcAwidp0WQ1vg=", "dev": true, "license": "WTFPL OR ISC", "dependencies": { @@ -1328,8 +1718,8 @@ }, "node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/semver/-/semver-5.7.2.tgz", + "integrity": "sha1-SNVdtzfDKHzUg14X+hP+rOHEHvg=", "dev": true, "license": "ISC", "bin": { @@ -1337,52 +1727,113 @@ } }, "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "version": "2.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=", "dev": true, + "license": "MIT", "dependencies": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "version": "3.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "version": "0.10.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/shelljs/-/shelljs-0.10.0.tgz", + "integrity": "sha1-47uumbDz8MxdzgW0ajRvriCQ6IM=", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" + "execa": "^5.1.1", + "fast-glob": "^3.3.2" }, "engines": { - "node": ">=4" + "node": ">=18" } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha1-w/z/nE2pMnhIczNeyXZfqU/2a8k=", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha1-wuC1oUpUCuvuO7xsP4ZmzJtQkSc=", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha1-1rtrN5Asb+9RdOX1M/q0xzKib0I=", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha1-Ed2hnVNo5Azp7CvcH7DsvAeQ7Oo=", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1390,41 +1841,55 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha1-qaF2f4r4QVURTqq9c/mSc8j1mtk=", + "dev": true, + "license": "ISC" }, "node_modules/strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha1-ibhS+y/L6Tb29LMYevsKEsGrWK0=", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8.0" } }, "node_modules/tr46": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true, + "license": "MIT" }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", "dev": true, "license": "WTFPL", "dependencies": { @@ -1433,18 +1898,20 @@ }, "node_modules/tunnel": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha1-cvExSzSlsZLbASMk3yzFh8pH+Sw=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, "node_modules/typed-rest-client": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.9.tgz", - "integrity": "sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g==", + "version": "1.8.11", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha1-aQbwLjyR6NhRV58lWr8P1ggAoE0=", "dev": true, + "license": "MIT", "dependencies": { "qs": "^6.9.1", "tunnel": "0.0.6", @@ -1452,70 +1919,84 @@ } }, "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", - "dev": true + "version": "1.13.8", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha1-qTohGGwEnb8OhHSW26cre9jB6Ss=", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha1-KTV6iee3ykrvO/D9P9DNc4hCKek=", + "dev": true, + "license": "MIT" }, "node_modules/universal-user-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz", - "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==", + "version": "6.0.1", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha1-FfIPVdo8kwxXvdvxc0xmVNX9Nao=", "dev": true, - "dependencies": { - "os-name": "^3.1.0" - } + "license": "ISC" }, "node_modules/utf8-byte-length": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha1-+fY5ENFVNu4rLV3UZlOJcV6sXB4=", "dev": true, "license": "(WTFPL OR MIT)" }, "node_modules/uuid": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha1-sj5DWK+oogL+ehAK8fX4g/AgB+4=", "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "dev": true, + "license": "MIT", "bin": { "uuid": "bin/uuid" } }, "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==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true, + "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==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "dev": true, + "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/which/-/which-2.0.2.tgz", + "integrity": "sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/windows-release": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", - "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha1-HBACfHIldD7sa4nfFg1kwuApOZk=", "dev": true, + "license": "MIT", "dependencies": { "execa": "^1.0.0" }, @@ -1526,1227 +2007,130 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - } - }, - "dependencies": { - "@octokit/auth-token": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", - "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "node_modules/windows-release/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha1-MNDvoHEt2361p24ehyG/+vprXVc=", "dev": true, - "requires": { - "@octokit/types": "^6.0.3" + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" } }, - "@octokit/core": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", - "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", + "node_modules/windows-release/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/execa/-/execa-1.0.0.tgz", + "integrity": "sha1-xiNqW7TfbW8V6I5/AXeYIWdJ3dg=", "dev": true, - "peer": true, - "requires": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.2.2", - "@octokit/request": "^9.2.3", - "@octokit/request-error": "^6.1.8", - "@octokit/types": "^14.0.0", - "before-after-hook": "^3.0.2", - "universal-user-agent": "^7.0.0" - }, + "license": "MIT", "dependencies": { - "@octokit/auth-token": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", - "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", - "dev": true, - "peer": true - }, - "@octokit/endpoint": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", - "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", - "dev": true, - "peer": true, - "requires": { - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.2" - } - }, - "@octokit/graphql": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", - "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", - "dev": true, - "peer": true, - "requires": { - "@octokit/request": "^9.2.3", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - } - }, - "@octokit/openapi-types": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", - "dev": true, - "peer": true - }, - "@octokit/request": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", - "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", - "dev": true, - "peer": true, - "requires": { - "@octokit/endpoint": "^10.1.4", - "@octokit/request-error": "^6.1.8", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^2.0.0", - "universal-user-agent": "^7.0.2" - } - }, - "@octokit/request-error": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", - "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", - "dev": true, - "peer": true, - "requires": { - "@octokit/types": "^14.0.0" - } - }, - "@octokit/types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", - "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", - "dev": true, - "peer": true, - "requires": { - "@octokit/openapi-types": "^25.0.0" - } - }, - "before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "dev": true, - "peer": true - }, - "universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", - "dev": true, - "peer": true - } + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" } }, - "@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "node_modules/windows-release/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha1-wbJVV189wh1Zv8ec09K0axw6VLU=", "dev": true, - "requires": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, + "license": "MIT", "dependencies": { - "universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true - } + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "@octokit/graphql": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", - "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "node_modules/windows-release/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true, - "requires": { - "@octokit/request": "^8.4.1", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/endpoint": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", - "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", - "dev": true, - "requires": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true - }, - "@octokit/request": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", - "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", - "dev": true, - "requires": { - "@octokit/endpoint": "^9.0.6", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/request-error": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", - "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", - "dev": true, - "requires": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, - "requires": { - "@octokit/openapi-types": "^24.2.0" - } - }, - "universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", - "dev": true - } + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", - "dev": true - }, - "@octokit/plugin-paginate-rest": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", - "integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==", - "dev": true, - "requires": { - "@octokit/types": "^2.0.1" - }, - "dependencies": { - "@octokit/types": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", - "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", - "dev": true, - "requires": { - "@types/node": ">= 8" - } - } - } - }, - "@octokit/plugin-request-log": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", - "dev": true, - "requires": {} - }, - "@octokit/plugin-rest-endpoint-methods": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", - "integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==", - "dev": true, - "requires": { - "@octokit/types": "^2.0.1", - "deprecation": "^2.3.1" - }, - "dependencies": { - "@octokit/types": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", - "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", - "dev": true, - "requires": { - "@types/node": ">= 8" - } - } - } - }, - "@octokit/request": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", - "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", - "dev": true, - "requires": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", - "dev": true, - "requires": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true - } - } - }, - "@octokit/request-error": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz", - "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==", - "dev": true, - "requires": { - "@octokit/types": "^2.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "dependencies": { - "@octokit/types": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", - "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", - "dev": true, - "requires": { - "@types/node": ">= 8" - } - } - } - }, - "@octokit/rest": { - "version": "16.43.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.2.tgz", - "integrity": "sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==", - "dev": true, - "requires": { - "@octokit/auth-token": "^2.4.0", - "@octokit/plugin-paginate-rest": "^1.1.1", - "@octokit/plugin-request-log": "^1.0.0", - "@octokit/plugin-rest-endpoint-methods": "2.4.0", - "@octokit/request": "^5.2.0", - "@octokit/request-error": "^1.0.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^2.0.0", - "btoa-lite": "^1.0.0", - "deprecation": "^2.0.0", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" - } - }, - "@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dev": true, - "requires": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true - }, - "@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "requires": { - "defer-to-connect": "^2.0.0" - } - }, - "@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "requires": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", - "dev": true - }, - "@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz", - "integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==", - "dev": true - }, - "@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, - "atob-lite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==", - "dev": true - }, - "azure-devops-node-api": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.0.0.tgz", - "integrity": "sha512-S6Il++7dQeMlZDokBDWw7YVoPeb90tWF10pYxnoauRMnkuL91jq9M7SOYRVhtO3FUC5URPkB/qzGa7jTLft0Xw==", - "dev": true, - "requires": { - "tunnel": "0.0.6", - "typed-rest-client": "^1.8.4" - } - }, - "azure-pipelines-task-lib": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-4.17.3.tgz", - "integrity": "sha512-UxfH5pk3uOHTi9TtLtdDyugQVkFES5A836ZEePjcs3jYyxm3EJ6IlFYq6gbfd6mNBhrM9fxG2u/MFYIJ+Z0cxQ==", - "dev": true, - "requires": { - "adm-zip": "^0.5.10", - "minimatch": "3.0.5", - "nodejs-file-downloader": "^4.11.1", - "q": "^1.5.1", - "semver": "^5.7.2", - "shelljs": "^0.8.5", - "uuid": "^3.0.1" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", - "dev": true - }, - "cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true - }, - "cacheable-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", - "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", - "dev": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "requires": { - "ms": "^2.1.3" - } - }, - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "requires": { - "mimic-response": "^3.1.0" - }, - "dependencies": { - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true - } - } - }, - "defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true - }, - "deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "fast-content-type-parse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", - "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", - "dev": true, - "peer": true - }, - "follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "requires": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true - }, - "http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, - "http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "requires": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true - }, - "is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", - "dev": true, - "requires": { - "json-buffer": "3.0.1" - } - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true - }, - "macos-release": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", - "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", - "dev": true - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true - }, - "minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-getopt": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/node-getopt/-/node-getopt-0.3.2.tgz", - "integrity": "sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==", - "dev": true - }, - "nodejs-file-downloader": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz", - "integrity": "sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ==", - "dev": true, - "requires": { - "follow-redirects": "^1.15.6", - "https-proxy-agent": "^5.0.0", - "mime-types": "^2.1.27", - "sanitize-filename": "^1.6.3" - } - }, - "normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true - }, - "npm-run-path": { + "node_modules/windows-release/node_modules/npm-run-path": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" } }, - "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true - }, - "octokit-pagination-methods": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", - "dev": true, - "requires": { - "macos-release": "^2.2.0", - "windows-release": "^3.1.0" - } - }, - "p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "dev": true - }, - "qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, - "responselike": { + "node_modules/windows-release/node_modules/path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true, - "requires": { - "lowercase-keys": "^2.0.0" - } - }, - "sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, - "requires": { - "truncate-utf8-bytes": "^1.0.0" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true - }, - "shebang-command": { + "node_modules/windows-release/node_modules/shebang-command": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true - }, - "shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", - "dev": true, - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "strip-eof": { + "node_modules/windows-release/node_modules/shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "requires": { - "utf8-byte-length": "^1.0.1" - } - }, - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true - }, - "typed-rest-client": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.9.tgz", - "integrity": "sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g==", - "dev": true, - "requires": { - "qs": "^6.9.1", - "tunnel": "0.0.6", - "underscore": "^1.12.1" - } - }, - "underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", - "dev": true - }, - "universal-user-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz", - "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true, - "requires": { - "os-name": "^3.1.0" - } - }, - "utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "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==", - "dev": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "which": { + "node_modules/windows-release/node_modules/which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/which/-/which-1.3.1.tgz", + "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", "dev": true, - "requires": { + "license": "ISC", + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "windows-release": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", - "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", - "dev": true, - "requires": { - "execa": "^1.0.0" - } - }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "resolved": "https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true, + "license": "ISC" } } } diff --git a/release/package.json b/release/package.json index ca49ec842d..54a87b9acb 100644 --- a/release/package.json +++ b/release/package.json @@ -4,7 +4,7 @@ "@octokit/rest": "^16.43.2", "@octokit/graphql": "^7.1.1", "azure-devops-node-api": "^12.0.0", - "azure-pipelines-task-lib": "^4.3.1", + "azure-pipelines-task-lib": "^5.2.8", "got": "^11.8.6", "node-getopt": "^0.3.2" } diff --git a/releaseNote.md b/releaseNote.md index c66f80b2bb..f68c1f225e 100644 --- a/releaseNote.md +++ b/releaseNote.md @@ -5,6 +5,7 @@ | -------------- | ------- | ------- | | Windows x64 | [vsts-agent-win-x64-.zip](https://download.agent.dev.azure.com/agent//vsts-agent-win-x64-.zip) | | | Windows x86 | [vsts-agent-win-x86-.zip](https://download.agent.dev.azure.com/agent//vsts-agent-win-x86-.zip) | | +| Windows ARM64 | [vsts-agent-win-arm64-.zip](https://download.agent.dev.azure.com/agent//vsts-agent-win-arm64-.zip) | | | macOS x64 | [vsts-agent-osx-x64-.tar.gz](https://download.agent.dev.azure.com/agent//vsts-agent-osx-x64-.tar.gz) | | | macOS ARM64 | [vsts-agent-osx-arm64-.tar.gz](https://download.agent.dev.azure.com/agent//vsts-agent-osx-arm64-.tar.gz) | | | Linux x64 | [vsts-agent-linux-x64-.tar.gz](https://download.agent.dev.azure.com/agent//vsts-agent-linux-x64-.tar.gz) | | @@ -29,6 +30,13 @@ C:\> mkdir myagent && cd myagent C:\myagent> Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory("$HOME\Downloads\vsts-agent-win-x86-.zip", "$PWD") ``` +## Windows ARM64 + +``` bash +C:\> mkdir myagent && cd myagent +C:\myagent> Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory("$HOME\Downloads\vsts-agent-win-arm64-.zip", "$PWD") +``` + ## macOS x64 ``` bash @@ -90,8 +98,9 @@ See [notes](docs/node6.md) on Node version support for more details. | | Package | SHA-256 | | ----------- | ------- | ------- | | Windows x64 | [pipelines-agent-win-x64-.zip](https://download.agent.dev.azure.com/agent//pipelines-agent-win-x64-.zip) | | -| Windows x86 | [pipelines-agent-win-x86-.zip](https://download.agent.dev.azure.com/agent//pipelines-agent-win-x86-.zip) | | -| macOS x64 | [pipelines-agent-osx-x64-.tar.gz](https://download.agent.dev.azure.com/agent//pipelines-agent-osx-x64-.tar.gz) | | +| Windows x86 | [pipelines-agent-win-x86-.zip](https://download.agent.dev.azure.com/agent//pipelines-agent-win-x86-.zip) | | +| Windows ARM64 | [pipelines-agent-win-arm64-.zip](https://download.agent.dev.azure.com/agent//pipelines-agent-win-arm64-.zip) | | +| macOS x64 | [pipelines-agent-osx-x64-.tar.gz](https://download.agent.dev.azure.com/agent//pipelines-agent-osx-x64-.tar.gz) | | | macOS ARM64 | [pipelines-agent-osx-arm64-.tar.gz](https://download.agent.dev.azure.com/agent//pipelines-agent-osx-arm64-.tar.gz) | | | Linux x64 | [pipelines-agent-linux-x64-.tar.gz](https://download.agent.dev.azure.com/agent//pipelines-agent-linux-x64-.tar.gz) | | | Linux ARM | [pipelines-agent-linux-arm-.tar.gz](https://download.agent.dev.azure.com/agent//pipelines-agent-linux-arm-.tar.gz) | | diff --git a/src/Agent.Listener/Agent.Listener.csproj b/src/Agent.Listener/Agent.Listener.csproj index 2ba302e683..14b36a4379 100644 --- a/src/Agent.Listener/Agent.Listener.csproj +++ b/src/Agent.Listener/Agent.Listener.csproj @@ -17,20 +17,23 @@ - + - + - - - + + + - + PreserveNewest + + PreserveNewest + diff --git a/src/Agent.Listener/Agent.cs b/src/Agent.Listener/Agent.cs index d5c5afb375..e9e76e39de 100644 --- a/src/Agent.Listener/Agent.cs +++ b/src/Agent.Listener/Agent.cs @@ -371,6 +371,10 @@ private async Task InitializeRuntimeFeatures() var enhancedLoggingFlag = await featureFlagProvider.GetFeatureFlagAsync(HostContext, "DistributedTask.Agent.UseEnhancedLogging", Trace); bool enhancedLoggingEnabled = string.Equals(enhancedLoggingFlag?.EffectiveState, "On", StringComparison.OrdinalIgnoreCase); + // Check enhanced worker crash handling feature flag + var enhancedWorkerCrashHandlingFlag = await featureFlagProvider.GetFeatureFlagAsync(HostContext, "DistributedTask.Agent.EnhancedWorkerCrashHandling", Trace); + bool enhancedWorkerCrashHandlingEnabled = string.Equals(enhancedWorkerCrashHandlingFlag?.EffectiveState, "On", StringComparison.OrdinalIgnoreCase); + Trace.Info($"Enhanced logging feature flag is {(enhancedLoggingEnabled ? "enabled" : "disabled")}"); // Set the result on TraceManager - this automatically switches all trace sources traceManager.SetEnhancedLoggingEnabled(enhancedLoggingEnabled); @@ -378,6 +382,18 @@ private async Task InitializeRuntimeFeatures() // Ensure child processes (worker/plugin) pick up enhanced logging via knob Environment.SetEnvironmentVariable("AZP_USE_ENHANCED_LOGGING", enhancedLoggingEnabled ? "true" : null); + // Check progressive backoff feature flag + var progressiveBackoffFlag = await featureFlagProvider.GetFeatureFlagAsync(HostContext, "DistributedTask.Agent.EnableProgressiveRetryBackoff", Trace); + bool progressiveBackoffEnabled = string.Equals(progressiveBackoffFlag?.EffectiveState, "On", StringComparison.OrdinalIgnoreCase); + + Trace.Info($"Progressive backoff feature flag is {(progressiveBackoffEnabled ? "enabled" : "disabled")}"); + // Ensure listener process picks up progressive backoff via knob + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", progressiveBackoffEnabled ? "true" : null); + + Trace.Info($"Enhanced worker crash handling feature flag is {(enhancedWorkerCrashHandlingEnabled ? "enabled" : "disabled")}"); + // Ensure child processes (worker/plugin) pick up enhanced crash handling via knob + Environment.SetEnvironmentVariable("AZP_ENHANCED_WORKER_CRASH_HANDLING", enhancedWorkerCrashHandlingEnabled ? "true" : null); + Trace.Info("Runtime features initialization completed successfully"); } catch (Exception ex) @@ -439,7 +455,7 @@ private async Task RunAsync(AgentSettings settings, bool runOnce = false) } else { - notification.StartClient(settings.NotificationPipeName, settings.MonitorSocketAddress, HostContext.AgentShutdownToken); + await notification.StartClient(settings.NotificationPipeName, settings.MonitorSocketAddress, HostContext.AgentShutdownToken); } // this is not a reliable way to disable auto update. // we need server side work to really enable the feature diff --git a/src/Agent.Listener/JobDispatcher.cs b/src/Agent.Listener/JobDispatcher.cs index b22acc3e6d..fc957025fb 100644 --- a/src/Agent.Listener/JobDispatcher.cs +++ b/src/Agent.Listener/JobDispatcher.cs @@ -401,10 +401,10 @@ private async Task RunAsync(Pipelines.AgentJobRequestMessage message, WorkerDisp // Because an agent can be idle for a long time between jobs, it is possible that in that time // a firewall has closed the connection. For that reason, forcibly reestablish this connection at the // start of a new job - Trace.Info(StringUtil.Format("Refreshing server connection before job execution [ConnectionType:JobRequest, Timeout:30s, JobId:{0}]", + Trace.Info(StringUtil.Format("Refreshing server connection before job execution [ConnectionType:JobRequest, JobId:{0}]", message.JobId)); var agentServer = HostContext.GetService(); - await agentServer.RefreshConnectionAsync(AgentConnectionType.JobRequest, TimeSpan.FromSeconds(30)); + await agentServer.RefreshConnectionAsync(AgentConnectionType.JobRequest); // start renew job request Trace.Info($"Start renew job request {requestId} for job {message.JobId}."); @@ -628,6 +628,10 @@ await processChannel.SendAsync( detailInfo = string.Join(Environment.NewLine, workerOutput); Trace.Info($"Return code {returnCode} indicate worker encounter an unhandled exception or app crash, attach worker stdout/stderr to JobRequest result."); await LogWorkerProcessUnhandledException(message, detailInfo, agentCertManager.SkipServerCertificateValidation); + + // Publish worker crash telemetry for Kusto analysis + var telemetryPublisher = HostContext.GetService(); + await telemetryPublisher.PublishWorkerCrashTelemetryAsync(HostContext, message.JobId, returnCode, "200"); } TaskResult result = TaskResultUtil.TranslateFromReturnCode(returnCode); @@ -641,8 +645,20 @@ await processChannel.SendAsync( await renewJobRequest; Trace.Info($"Job request completion initiated - Completing job request for job: {message.JobId}"); - // complete job request - await CompleteJobRequestAsync(_poolId, message, lockToken, result, detailInfo); + + if (ShouldUseEnhancedCrashHandling(message, returnCode)) + { + // Direct plan event reporting for Plan v8+ worker crashes + await ReportJobCompletionEventAsync(message, result, agentCertManager.SkipServerCertificateValidation); + Trace.Info("Plan event reporting executed successfully for worker crash"); + } + else + { + // Standard completion for Plan v7 or normal Plan v8+ scenarios, or when enhanced handling is disabled + await CompleteJobRequestAsync(_poolId, message, lockToken, result, detailInfo); + Trace.Info("Standard completion executed successfully"); + } + Trace.Info("Job request completion completed"); // print out unhandled exception happened in worker after we complete job request. @@ -816,9 +832,9 @@ public async Task RenewJobRequestAsync(int poolId, long requestId, Guid lockToke if (encounteringError > 0) { encounteringError = 0; - agentServer.SetConnectionTimeout(AgentConnectionType.JobRequest, TimeSpan.FromSeconds(60)); + agentServer.ResetConnectionTimeout(AgentConnectionType.JobRequest); HostContext.WritePerfCounter("JobRenewRecovered"); - Trace.Info(StringUtil.Format("Job renewal error recovery completed [RequestId:{0}, ErrorsCleared:True, ConnectionTimeout:60s, Status:Recovered]", + Trace.Info(StringUtil.Format("Job renewal error recovery completed [RequestId:{0}, ErrorsCleared:True, Status:Recovered]", requestId)); } @@ -888,7 +904,7 @@ public async Task RenewJobRequestAsync(int poolId, long requestId, Guid lockToke } // Re-establish connection to server in order to avoid affinity with server. - // Reduce connection timeout to 30 seconds (from 60s) + // Reduce connection timeout to 30 seconds for faster failure detection during retries HostContext.WritePerfCounter("ResetJobRenewConnection"); Trace.Info(StringUtil.Format("Job renewal connection refresh initiated [RequestId:{0}, Timeout:30s, Reason:RetryRecovery, ErrorCount:{1}]", requestId, encounteringError)); @@ -971,55 +987,146 @@ private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequest throw new AggregateException(exceptions); } - // log an error issue to job level timeline record - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope", MessageId = "jobServer")] - private async Task LogWorkerProcessUnhandledException(Pipelines.AgentJobRequestMessage message, string errorMessage, bool skipServerCertificateValidation = false) + // Determines if enhanced crash handling should be used for Plan v8+ worker crashes + private bool ShouldUseEnhancedCrashHandling(Pipelines.AgentJobRequestMessage message, int returnCode) { - try + if (!AgentKnobs.EnhancedWorkerCrashHandling.GetValue(UtilKnobValueContext.Instance()).AsBoolean()) + return false; + + bool isPlanV8Plus = PlanUtil.GetFeatures(message.Plan).HasFlag(PlanFeatures.JobCompletedPlanEvent); + bool isWorkerCrash = !TaskResultUtil.IsValidReturnCode(returnCode); + + return isPlanV8Plus && isWorkerCrash; + } + + // Creates a job server connection with proper URL normalization for OnPremises servers + private async Task CreateJobServerConnectionAsync(Pipelines.AgentJobRequestMessage message, bool skipServerCertificateValidation = false) + { + Trace.Info("Creating job server connection"); + + var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection)); + ArgUtil.NotNull(systemConnection, nameof(systemConnection)); + + var jobServer = HostContext.GetService(); + VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection); + Uri jobServerUrl = systemConnection.Url; + + Trace.Verbose($"Initial connection details [JobId:{message.JobId}, OriginalUrl:{jobServerUrl}]"); + + // Make sure SystemConnection Url match Config Url base for OnPremises server + if (!message.Variables.ContainsKey(Constants.Variables.System.ServerType) || + string.Equals(message.Variables[Constants.Variables.System.ServerType]?.Value, "OnPremises", StringComparison.OrdinalIgnoreCase)) { - var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection)); - ArgUtil.NotNull(systemConnection, nameof(systemConnection)); + try + { + Uri urlResult = null; + Uri configUri = new Uri(_agentSetting.ServerUrl); + if (Uri.TryCreate(new Uri(configUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)), jobServerUrl.PathAndQuery, out urlResult)) + { + //replace the schema and host portion of messageUri with the host from the + //server URI (which was set at config time) + Trace.Info($"URL replacement for OnPremises server - Original: {jobServerUrl}, New: {urlResult}"); + jobServerUrl = urlResult; + } + } + catch (InvalidOperationException ex) + { + Trace.Error(ex); + } + catch (UriFormatException ex) + { + Trace.Error(ex); + } + } + + var jobConnection = VssUtil.CreateConnection(jobServerUrl, jobServerCredential, trace: Trace, skipServerCertificateValidation); + await jobServer.ConnectAsync(jobConnection); + Trace.Info($"Job server connection established successfully"); + + return jobConnection; + } - var jobServer = HostContext.GetService(); - VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection); - Uri jobServerUrl = systemConnection.Url; + // Reports job completion to server via plan event (similar to how worker reports) + // Used for Plan v8+ scenarios where listener needs to notify server of job completion + private async Task ReportJobCompletionEventAsync(Pipelines.AgentJobRequestMessage message, TaskResult result, bool skipServerCertificateValidation = false) + { + Trace.Info($"Plan event reporting initiated - Sending job completion event to server"); - // Make sure SystemConnection Url match Config Url base for OnPremises server - if (!message.Variables.ContainsKey(Constants.Variables.System.ServerType) || - string.Equals(message.Variables[Constants.Variables.System.ServerType]?.Value, "OnPremises", StringComparison.OrdinalIgnoreCase)) + try + { + using (var jobConnection = await CreateJobServerConnectionAsync(message, skipServerCertificateValidation)) { - try + var jobServer = HostContext.GetService(); + // Create job completed event (similar to worker) + var jobCompletedEvent = new JobCompletedEvent(message.RequestId, message.JobId, result); + + // Send plan event with retry logic (similar to worker pattern) + int retryLimit = 5; + var exceptions = new List(); + + while (retryLimit-- > 0) { - Uri result = null; - Uri configUri = new Uri(_agentSetting.ServerUrl); - if (Uri.TryCreate(new Uri(configUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)), jobServerUrl.PathAndQuery, out result)) + try { - //replace the schema and host portion of messageUri with the host from the - //server URI (which was set at config time) - jobServerUrl = result; + await jobServer.RaisePlanEventAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, jobCompletedEvent, CancellationToken.None); + Trace.Info($"Plan event reporting completed successfully [JobId:{message.JobId}, Result:{result}]"); + return; } + catch (TaskOrchestrationPlanNotFoundException ex) + { + Trace.Error($"TaskOrchestrationPlanNotFoundException during plan event reporting for job {message.JobId}"); + Trace.Error(ex); + return; // No point retrying + } + catch (TaskOrchestrationPlanSecurityException ex) + { + Trace.Error($"TaskOrchestrationPlanSecurityException during plan event reporting for job {message.JobId}"); + Trace.Error(ex); + return; // No point retrying + } + catch (Exception ex) + { + Trace.Error(ex); + exceptions.Add(ex); + } + + // delay 5 seconds before next retry + Trace.Info($"Plan event reporting retry delay - Waiting 5 seconds before retry {5 - retryLimit}/5"); + await Task.Delay(TimeSpan.FromSeconds(5)); } - catch (InvalidOperationException ex) - { - //cannot parse the Uri - not a fatal error - Trace.Error(ex); - } - catch (UriFormatException ex) + + // If we get here, all retries failed + Trace.Warning($"Plan event reporting failed after all retries [JobId:{message.JobId}, TotalExceptions:{exceptions.Count}]"); + foreach (var ex in exceptions) { - //cannot parse the Uri - not a fatal error Trace.Error(ex); } } + } + catch (Exception ex) + { + Trace.Error("Critical error during plan event reporting setup"); + Trace.Error(ex); + } + } - var jobConnection = VssUtil.CreateConnection(jobServerUrl, jobServerCredential, trace: Trace, skipServerCertificateValidation); - await jobServer.ConnectAsync(jobConnection); - var timeline = await jobServer.GetTimelineAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, CancellationToken.None); - ArgUtil.NotNull(timeline, nameof(timeline)); - TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job"); - ArgUtil.NotNull(jobRecord, nameof(jobRecord)); - jobRecord.ErrorCount++; - jobRecord.Issues.Add(new Issue() { Type = IssueType.Error, Message = errorMessage }); - await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None); + // log an error issue to job level timeline record + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope", MessageId = "jobServer")] + private async Task LogWorkerProcessUnhandledException(Pipelines.AgentJobRequestMessage message, string errorMessage, bool skipServerCertificateValidation = false) + { + try + { + using (var jobConnection = await CreateJobServerConnectionAsync(message, skipServerCertificateValidation)) + { + var jobServer = HostContext.GetService(); + var timeline = await jobServer.GetTimelineAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, CancellationToken.None); + ArgUtil.NotNull(timeline, nameof(timeline)); + TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job"); + ArgUtil.NotNull(jobRecord, nameof(jobRecord)); + jobRecord.ErrorCount++; + jobRecord.Issues.Add(new Issue() { Type = IssueType.Error, Message = errorMessage }); + await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None); + } } catch (SocketException ex) { diff --git a/src/Agent.Listener/MessageListener.cs b/src/Agent.Listener/MessageListener.cs index e67fe85102..4bfe88e1cf 100644 --- a/src/Agent.Listener/MessageListener.cs +++ b/src/Agent.Listener/MessageListener.cs @@ -18,7 +18,9 @@ using Microsoft.VisualStudio.Services.OAuth; using System.Diagnostics; using System.Runtime.InteropServices; +using Agent.Sdk.Knob; using Agent.Sdk.Util; +using Agent.Listener.Configuration; namespace Microsoft.VisualStudio.Services.Agent.Listener { @@ -39,11 +41,17 @@ public sealed class MessageListener : AgentService, IMessageListener private ITerminal _term; private IAgentServer _agentServer; private TaskAgentSession _session; + private static UtilKnobValueContext _knobContext = UtilKnobValueContext.Instance(); private TimeSpan _getNextMessageRetryInterval; - private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30); + private TimeSpan _keepAliveRetryInterval; + private bool? _enableProgressiveBackoff = null; + private TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30); private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4); private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30); private readonly Dictionary _sessionCreationExceptionTracker = new Dictionary(); + private TimeSpan _sessionConflictElapsedTime = TimeSpan.Zero; + private TimeSpan _clockSkewElapsedTime = TimeSpan.Zero; + public override void Initialize(IHostContext hostContext) { @@ -53,10 +61,50 @@ public override void Initialize(IHostContext hostContext) _agentServer = HostContext.GetService(); } + /// + /// Calculates the retry interval based on the progressive backoff setting and error count. + /// + /// The number of consecutive errors. + /// The default interval when progressive backoff is disabled. + /// The current interval (used for random backoff calculation). + /// If true, uses random backoff when progressive backoff is disabled. + /// The calculated retry interval. + private TimeSpan GetRetryInterval( + int continuousError, + TimeSpan defaultInterval, + TimeSpan currentInterval = default, + bool useRandomBackoff = false) + { + if (_enableProgressiveBackoff == true) + { + double delaySeconds = Math.Min(Math.Pow(2, continuousError) * 1.5, 300); + return TimeSpan.FromSeconds(delaySeconds); + } + + if (useRandomBackoff) + { + // Random backoff for GetNextMessage: [15,30]s for first 5 errors, then [30,60]s + var minBackoff = continuousError <= 5 + ? TimeSpan.FromSeconds(15) + : TimeSpan.FromSeconds(30); + var maxBackoff = continuousError <= 5 + ? TimeSpan.FromSeconds(30) + : TimeSpan.FromSeconds(60); + return BackoffTimerHelper.GetRandomBackoff(minBackoff, maxBackoff, currentInterval); + } + + // Default: fixed interval + return defaultInterval; + } + public async Task CreateSessionAsync(CancellationToken token) { Trace.Entering(); + // Fetch progressive backoff knob value + _enableProgressiveBackoff = AgentKnobs.EnableProgressiveRetryBackoff.GetValue(_knobContext).AsBoolean(); + Trace.Info($"Progressive backoff knob value: {_enableProgressiveBackoff}"); + // Settings var configManager = HostContext.GetService(); _settings = configManager.LoadSettings(); @@ -84,8 +132,10 @@ public async Task CreateSessionAsync(CancellationToken token) string errorMessage = string.Empty; bool encounteringError = false; + int continuousError = 0; _term.WriteLine(StringUtil.Loc("ConnectToServer")); + while (true) { token.ThrowIfCancellationRequested(); @@ -102,11 +152,20 @@ public async Task CreateSessionAsync(CancellationToken token) token); Trace.Info($"Session created."); + + if (_enableProgressiveBackoff == true) + { + // Reset BOTH on successful session creation + _sessionConflictElapsedTime = TimeSpan.Zero; + _clockSkewElapsedTime = TimeSpan.Zero; + } + if (encounteringError) { _term.WriteLine(StringUtil.Loc("QueueConnected", DateTime.UtcNow)); _sessionCreationExceptionTracker.Clear(); encounteringError = false; + continuousError = 0; } return true; @@ -137,12 +196,19 @@ public async Task CreateSessionAsync(CancellationToken token) return false; } + continuousError++; + + _sessionCreationRetryInterval = GetRetryInterval( + continuousError, + defaultInterval: TimeSpan.FromSeconds(30)); + if (!encounteringError) //print the message only on the first error { _term.WriteError(StringUtil.Loc("QueueConError", DateTime.UtcNow, ex.Message, _sessionCreationRetryInterval.TotalSeconds)); encounteringError = true; } + Trace.Info($"Unable to create session in CreateSessionAsync (attempt {continuousError})"); Trace.Info(StringUtil.Format("Sleeping for {0} seconds before retrying.", _sessionCreationRetryInterval.TotalSeconds)); await HostContext.Delay(_sessionCreationRetryInterval, token); } @@ -170,6 +236,11 @@ public async Task GetNextMessageAsync(CancellationToken token) string errorMessage = string.Empty; Stopwatch heartbeat = new Stopwatch(); heartbeat.Restart(); + + // Fetch progressive backoff knob value + _enableProgressiveBackoff = AgentKnobs.EnableProgressiveRetryBackoff.GetValue(_knobContext).AsBoolean(); + Trace.Info($"Progressive backoff knob value: {_enableProgressiveBackoff}"); + while (true) { token.ThrowIfCancellationRequested(); @@ -223,18 +294,12 @@ public async Task GetNextMessageAsync(CancellationToken token) else { continuousError++; - //retry after a random backoff to avoid service throttling - //in case of there is a service error happened and all agents get kicked off of the long poll and all agent try to reconnect back at the same time. - if (continuousError <= 5) - { - // random backoff [15, 30] - _getNextMessageRetryInterval = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(30), _getNextMessageRetryInterval); - } - else - { - // more aggressive backoff [30, 60] - _getNextMessageRetryInterval = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(60), _getNextMessageRetryInterval); - } + + _getNextMessageRetryInterval = GetRetryInterval( + continuousError, + defaultInterval: TimeSpan.FromSeconds(30), + currentInterval: _getNextMessageRetryInterval, + useRandomBackoff: true); if (!encounteringError) { @@ -244,8 +309,9 @@ public async Task GetNextMessageAsync(CancellationToken token) } // re-create VssConnection before next retry - await _agentServer.RefreshConnectionAsync(AgentConnectionType.MessageQueue, TimeSpan.FromSeconds(60)); - + await _agentServer.RefreshConnectionAsync(AgentConnectionType.MessageQueue); + + Trace.Info($"Unable to get next message in GetNextMessageAsync (attempt {continuousError})"); Trace.Info(StringUtil.Format("Sleeping for {0} seconds before retrying.", _getNextMessageRetryInterval.TotalSeconds)); await HostContext.Delay(_getNextMessageRetryInterval, token); } @@ -290,20 +356,37 @@ public async Task DeleteMessageAsync(TaskAgentMessage message) public async Task KeepAlive(CancellationToken token) { - + int continuousError = 0; + _keepAliveRetryInterval = TimeSpan.FromSeconds(30); + + // Fetch progressive backoff knob value + _enableProgressiveBackoff = AgentKnobs.EnableProgressiveRetryBackoff.GetValue(_knobContext).AsBoolean(); + Trace.Info($"Progressive backoff knob value: {_enableProgressiveBackoff}"); + while (!token.IsCancellationRequested) { try { await _agentServer.GetAgentMessageAsync(_settings.PoolId, _session.SessionId, null, token); Trace.Info($"Sent GetAgentMessage to keep alive agent {_settings.AgentId}, session '{_session.SessionId}'."); + + // Reset on success + continuousError = 0; + _keepAliveRetryInterval = TimeSpan.FromSeconds(30); } catch (Exception ex) { - Trace.Verbose("Unable to sent GetAgentMessage to keep alive", ex.Message); + continuousError++; + + _keepAliveRetryInterval = GetRetryInterval( + continuousError, + defaultInterval: TimeSpan.FromSeconds(30)); + + Trace.Info($"Unable to sent GetAgentMessage to keep alive (attempt {continuousError}): {ex.Message}"); + Trace.Info($"KeepAlive will retry in {_keepAliveRetryInterval.TotalSeconds} seconds."); } - await HostContext.Delay(TimeSpan.FromSeconds(30), token); + await HostContext.Delay(_keepAliveRetryInterval, token); } } private TaskAgentMessage DecryptMessage(TaskAgentMessage message) @@ -378,19 +461,38 @@ private bool IsSessionCreationExceptionRetriable(Exception ex) { Trace.Info("The session for this agent already exists."); _term.WriteError(StringUtil.Loc("SessionExist")); - if (_sessionCreationExceptionTracker.ContainsKey(nameof(TaskAgentSessionConflictException))) - { - _sessionCreationExceptionTracker[nameof(TaskAgentSessionConflictException)]++; - if (_sessionCreationExceptionTracker[nameof(TaskAgentSessionConflictException)] * _sessionCreationRetryInterval.TotalSeconds >= _sessionConflictRetryLimit.TotalSeconds) + + // when the EnableProgressiveRetryBackoff FF is enabled + if(_enableProgressiveBackoff == true){ + + //update session conflict time + _sessionConflictElapsedTime += _sessionCreationRetryInterval; + + //check the total elapsed time is within the retry limit + if (_sessionConflictElapsedTime >= _sessionConflictRetryLimit) { - Trace.Info("The session conflict exception have reached retry limit."); + Trace.Info($"The session conflict exception have reached retry limit. Elapsed: {_sessionConflictElapsedTime.TotalSeconds}s, Limit: {_sessionConflictRetryLimit.TotalSeconds}s"); _term.WriteError(StringUtil.Loc("SessionExistStopRetry", _sessionConflictRetryLimit.TotalSeconds)); return false; } + } - else - { - _sessionCreationExceptionTracker[nameof(TaskAgentSessionConflictException)] = 1; + // when the EnableProgressiveRetryBackoff FF is disabled + else{ + if (_sessionCreationExceptionTracker.ContainsKey(nameof(TaskAgentSessionConflictException))) + { + _sessionCreationExceptionTracker[nameof(TaskAgentSessionConflictException)]++; + if (_sessionCreationExceptionTracker[nameof(TaskAgentSessionConflictException)] * _sessionCreationRetryInterval.TotalSeconds >= _sessionConflictRetryLimit.TotalSeconds) + { + Trace.Info("The session conflict exception have reached retry limit."); + _term.WriteError(StringUtil.Loc("SessionExistStopRetry", _sessionConflictRetryLimit.TotalSeconds)); + return false; + } + } + else + { + _sessionCreationExceptionTracker[nameof(TaskAgentSessionConflictException)] = 1; + } } Trace.Info("The session conflict exception haven't reached retry limit."); @@ -400,21 +502,39 @@ private bool IsSessionCreationExceptionRetriable(Exception ex) { Trace.Info("Local clock might skewed."); _term.WriteError(StringUtil.Loc("LocalClockSkewed")); - if (_sessionCreationExceptionTracker.ContainsKey(nameof(VssOAuthTokenRequestException))) + + // when the EnableProgressiveRetryBackoff FF is enabled + if(_enableProgressiveBackoff == true) { - _sessionCreationExceptionTracker[nameof(VssOAuthTokenRequestException)]++; - if (_sessionCreationExceptionTracker[nameof(VssOAuthTokenRequestException)] * _sessionCreationRetryInterval.TotalSeconds >= _clockSkewRetryLimit.TotalSeconds) + // Only update clock skew time + _clockSkewElapsedTime += _sessionCreationRetryInterval; + + // check the total elapsed time is within the retry limit + if (_clockSkewElapsedTime >= _clockSkewRetryLimit) { - Trace.Info("The OAuth token request exception have reached retry limit."); + Trace.Info($"The OAuth token request exception have reached retry limit. Elapsed: {_clockSkewElapsedTime.TotalSeconds}s, Limit: {_clockSkewRetryLimit.TotalSeconds}s"); _term.WriteError(StringUtil.Loc("ClockSkewStopRetry", _clockSkewRetryLimit.TotalSeconds)); return false; } } + // when the EnableProgressiveRetryBackoff FF is disabled else { - _sessionCreationExceptionTracker[nameof(VssOAuthTokenRequestException)] = 1; + if (_sessionCreationExceptionTracker.ContainsKey(nameof(VssOAuthTokenRequestException))) + { + _sessionCreationExceptionTracker[nameof(VssOAuthTokenRequestException)]++; + if (_sessionCreationExceptionTracker[nameof(VssOAuthTokenRequestException)] * _sessionCreationRetryInterval.TotalSeconds >= _clockSkewRetryLimit.TotalSeconds) + { + Trace.Info("The OAuth token request exception have reached retry limit."); + _term.WriteError(StringUtil.Loc("ClockSkewStopRetry", _clockSkewRetryLimit.TotalSeconds)); + return false; + } + } + else + { + _sessionCreationExceptionTracker[nameof(VssOAuthTokenRequestException)] = 1; + } } - Trace.Info("The OAuth token request exception haven't reached retry limit."); return true; } diff --git a/src/Agent.Listener/SelfUpdater.cs b/src/Agent.Listener/SelfUpdater.cs index 6f0d19012e..2485c0b5ec 100644 --- a/src/Agent.Listener/SelfUpdater.cs +++ b/src/Agent.Listener/SelfUpdater.cs @@ -103,12 +103,13 @@ public async Task SelfUpdate(AgentRefreshMessage updateMessage, IJobDispat if (PlatformUtil.RunningOnWindows) { invokeScript.StartInfo.FileName = WhichUtil.Which("cmd.exe", trace: Trace); - invokeScript.StartInfo.Arguments = $"/c \"{updateScript}\""; + invokeScript.StartInfo.ArgumentList.Add("/c"); + invokeScript.StartInfo.ArgumentList.Add(updateScript); } else { invokeScript.StartInfo.FileName = WhichUtil.Which("bash", trace: Trace); - invokeScript.StartInfo.Arguments = $"\"{updateScript}\""; + invokeScript.StartInfo.ArgumentList.Add(updateScript); } invokeScript.Start(); Trace.Info($"Update script start running"); diff --git a/src/Agent.Listener/Telemetry/WorkerCrashTelemetryPublisher.cs b/src/Agent.Listener/Telemetry/WorkerCrashTelemetryPublisher.cs new file mode 100644 index 0000000000..16f2038f9f --- /dev/null +++ b/src/Agent.Listener/Telemetry/WorkerCrashTelemetryPublisher.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Newtonsoft.Json; + +namespace Microsoft.VisualStudio.Services.Agent.Listener.Telemetry +{ + [ServiceLocator(Default = typeof(WorkerCrashTelemetryPublisher))] + public interface IWorkerCrashTelemetryPublisher : IAgentService + { + Task PublishWorkerCrashTelemetryAsync(IHostContext hostContext, Guid jobId, int exitCode, string tracePoint); + } + + public sealed class WorkerCrashTelemetryPublisher : AgentService, IWorkerCrashTelemetryPublisher + { + public async Task PublishWorkerCrashTelemetryAsync(IHostContext hostContext, Guid jobId, int exitCode, string tracePoint) + { + try + { + var telemetryPublisher = hostContext.GetService(); + + var telemetryData = new Dictionary + { + ["JobId"] = jobId.ToString(), + ["ExitCode"] = exitCode.ToString(), + ["TracePoint"] = tracePoint + }; + + var command = new Command("telemetry", "publish") + { + Data = JsonConvert.SerializeObject(telemetryData) + }; + command.Properties.Add("area", "AzurePipelinesAgent"); + command.Properties.Add("feature", "WorkerCrash"); + + await telemetryPublisher.PublishEvent(hostContext, command); + Trace.Info($"Published worker crash telemetry for job {jobId} with exit code {exitCode}"); + } + catch (Exception ex) + { + Trace.Warning($"Failed to publish worker crash telemetry: {ex}"); + } + } + } +} \ No newline at end of file diff --git a/src/Agent.Listener/net10.json b/src/Agent.Listener/net10.json new file mode 100644 index 0000000000..dd6f18ebe1 --- /dev/null +++ b/src/Agent.Listener/net10.json @@ -0,0 +1,160 @@ +[ + { + "id": "alpine", + "versions": [ + { + "name": "3.22+" + } + ] + }, + { + "id": "amzn", + "versions": [ + { + "name": "2" + }, + { + "name": "2023" + } + ] + }, + { + "id": "azure-linux", + "versions": [ + { + "name": "3.0+" + } + ] + }, + { + "id": "centos", + "versions": [ + { + "name": "9+" + } + ] + }, + { + "id": "debian", + "versions": [ + { + "name": "12+" + } + ] + }, + { + "id": "fedora", + "versions": [ + { + "name": "42+" + } + ] + }, + { + "id": "macOS", + "versions": [ + { + "name": "14+" + } + ] + }, + { + "id": "opensuse-leap", + "versions": [ + { + "name": "15.6+" + } + ] + }, + { + "id": "almalinux", + "versions": [ + { + "name": "8+" + } + ] + }, + { + "id": "rhel", + "versions": [ + { + "name": "8+" + } + ] + }, + { + "id": "ol", + "versions": [ + { + "name": "8+" + } + ] + }, + { + "id": "rocky", + "versions": [ + { + "name": "8+" + } + ] + }, + { + "id": "sles", + "versions": [ + { + "name": "15.6+" + } + ] + }, + { + "id": "ubuntu", + "versions": [ + { + "name": "22.04" + }, + { + "name": "24.04" + }, + { + "name": "25.10" + } + ] + }, + { + "id": "Windows Client", + "versions": [ + { + "name": "10", + "version": "1607+" + }, + { + "name": "11", + "version": "22631+" + } + ] + }, + { + "id": "Windows Nano Server", + "versions": [ + { + "name": "2019+" + } + ] + }, + { + "id": "Windows Server", + "versions": [ + { + "name": "2016+" + } + ] + }, + { + "id": "Windows Server Core", + "versions": [ + { + "name": "2016+" + } + ] + } +] diff --git a/src/Agent.Listener/net8.json b/src/Agent.Listener/net8.json index 2d2600cd66..ba3cd07eb6 100644 --- a/src/Agent.Listener/net8.json +++ b/src/Agent.Listener/net8.json @@ -3,7 +3,7 @@ "id": "alpine", "versions": [ { - "name": "3.17+" + "name": "3.20+" } ] }, @@ -19,23 +19,23 @@ ] }, { - "id": "debian", + "id": "azure-linux", "versions": [ { - "name": "11+" + "name": "3.0" } ] }, { - "id": "fedora", + "id": "centos", "versions": [ { - "name": "39+" + "name": "9+" } ] }, { - "id": "macOS", + "id": "debian", "versions": [ { "name": "12+" @@ -43,10 +43,18 @@ ] }, { - "id": "mariner", + "id": "fedora", + "versions": [ + { + "name": "41+" + } + ] + }, + { + "id": "macOS", "versions": [ { - "name": "2.0+" + "name": "14+" } ] }, @@ -54,7 +62,7 @@ "id": "opensuse-leap", "versions": [ { - "name": "15.5+" + "name": "15.6+" } ] }, @@ -94,24 +102,21 @@ "id": "sles", "versions": [ { - "name": "15+" + "name": "15.6+" } ] }, { "id": "ubuntu", "versions": [ - { - "name": "20.04" - }, { "name": "22.04" }, { - "name": "23.10" + "name": "24.04" }, { - "name": "24.04" + "name": "25.10" } ] }, @@ -123,7 +128,8 @@ "version": "1607+" }, { - "name": "11" + "name": "11", + "version": "22631+" } ] }, @@ -131,7 +137,7 @@ "id": "Windows Nano Server", "versions": [ { - "version": "1809+" + "name": "2019+" } ] }, diff --git a/src/Agent.PluginHost/Agent.PluginHost.csproj b/src/Agent.PluginHost/Agent.PluginHost.csproj index a1f399343b..143cf9c544 100644 --- a/src/Agent.PluginHost/Agent.PluginHost.csproj +++ b/src/Agent.PluginHost/Agent.PluginHost.csproj @@ -11,12 +11,12 @@ - + - - - + + + diff --git a/src/Agent.Plugins/Agent.Plugins.csproj b/src/Agent.Plugins/Agent.Plugins.csproj index 1d88f68007..a37e090cda 100644 --- a/src/Agent.Plugins/Agent.Plugins.csproj +++ b/src/Agent.Plugins/Agent.Plugins.csproj @@ -15,10 +15,10 @@ - + - - - + + + diff --git a/src/Agent.Plugins/Artifact/FileContainerProvider.cs b/src/Agent.Plugins/Artifact/FileContainerProvider.cs index b2ad152cc9..cb3e2a301c 100644 --- a/src/Agent.Plugins/Artifact/FileContainerProvider.cs +++ b/src/Agent.Plugins/Artifact/FileContainerProvider.cs @@ -503,10 +503,13 @@ private void ExtractTar(string tarArchivePath, string extractedFilesDir) Directory.CreateDirectory(extractedFilesDir); var extractionProcessInfo = new ProcessStartInfo("tar") { - Arguments = $"xf {tarArchivePath} --directory {extractedFilesDir}", UseShellExecute = false, RedirectStandardError = true }; + extractionProcessInfo.ArgumentList.Add("xf"); + extractionProcessInfo.ArgumentList.Add(tarArchivePath); + extractionProcessInfo.ArgumentList.Add("--directory"); + extractionProcessInfo.ArgumentList.Add(extractedFilesDir); Process extractionProcess = Process.Start(extractionProcessInfo); extractionProcess.WaitForExit(); diff --git a/src/Agent.Plugins/GitCliManager.cs b/src/Agent.Plugins/GitCliManager.cs index b3a9d72a54..0c62d95697 100644 --- a/src/Agent.Plugins/GitCliManager.cs +++ b/src/Agent.Plugins/GitCliManager.cs @@ -447,7 +447,7 @@ public async Task GitSubmoduleReset(AgentTaskPluginExecutionContext context } // git submodule update --init --force [--depth=15] [--recursive] - public async Task GitSubmoduleUpdate(AgentTaskPluginExecutionContext context, string repositoryPath, int fetchDepth, string additionalCommandLine, bool recursive, CancellationToken cancellationToken) + public async Task GitSubmoduleUpdate(AgentTaskPluginExecutionContext context, string repositoryPath, int fetchDepth, IEnumerable filters, string additionalCommandLine, bool recursive, CancellationToken cancellationToken) { context.Debug("Update the registered git submodules."); string options = "update --init --force"; @@ -455,6 +455,11 @@ public async Task GitSubmoduleUpdate(AgentTaskPluginExecutionContext contex { options = options + $" --depth={fetchDepth}"; } + if (AgentKnobs.UseFetchFilterInGitSubmoduleUpdate.GetValue(context).AsBoolean() && filters != null) + { + options += " " + string.Join(" ", filters.Select(f => "--filter=" + f)); + } + if (recursive) { options = options + " --recursive"; diff --git a/src/Agent.Plugins/GitSourceProvider.cs b/src/Agent.Plugins/GitSourceProvider.cs index 2ae47cc71f..a387a304c8 100644 --- a/src/Agent.Plugins/GitSourceProvider.cs +++ b/src/Agent.Plugins/GitSourceProvider.cs @@ -618,6 +618,35 @@ public async Task GetSourceAsync( } } + string agentWorkFolder = executionContext.Variables.GetValueOrDefault("agent.workfolder")?.Value; + if (!string.IsNullOrEmpty(agentWorkFolder) + && File.Exists(Path.Combine(agentWorkFolder, ".autoManagedVhd")) + && !AgentKnobs.DisableAutoManagedVhdShallowOverride.GetValue(executionContext).AsBoolean()) + { + // The existing working directory comes from an AutoManagedVHD (indicated by the + // .autoManagedVhd marker file placed in the agent work folder). + // An AutoManagedVHD always contains a full, non-shallow clone of the repository. + // Some pipelines enable shallow fetch parameters (e.g., fetchDepth > 0). However, + // Git cannot convert an existing full clone into a shallow one in-place. + // + // Technical reason: + // A full clone already has complete commit history and object reachability. + // When a fetch is issued with a non-zero --depth against a full clone, Git + // does *not* rewrite the local history to match the requested shallow boundary. + // Instead, to honor the depth constraint, Git falls back to creating a brand-new + // shallow clone in an empty directory. + // + // That behavior causes the agent to discard the VHD-provided clone and re-clone + // from scratch—defeating the whole purpose of using AutoManagedVHDs for fast sync. + // + // To avoid this, force a normal full fetch by disabling shallow behavior: + // clean = false → preserve the VHD clone + // fetchDepth = 0 → perform a standard "sync" fetch + clean = false; + fetchDepth = 0; + executionContext.Output($"Detected an Auto Managed VHD at {targetPath}. Setting clean to false and fetchDepth to 0."); + } + // When repo.clean is selected for a git repo, execute git clean -ffdx and git reset --hard HEAD on the current repo. // This will help us save the time to reclone the entire repo. // If any git commands exit with non-zero return code or any exception happened during git.exe invoke, fall back to delete the repo folder. @@ -745,7 +774,7 @@ public async Task GetSourceAsync( executionContext.Warning("Unable turn off git auto garbage collection, git fetch operation may trigger auto garbage collection which will affect the performance of fetching."); } - SetGitFeatureFlagsConfiguration(executionContext, gitCommandManager, targetPath); + await SetGitFeatureFlagsConfiguration(executionContext, gitCommandManager, targetPath); // always remove any possible left extraheader setting from git config. if (await gitCommandManager.GitConfigExist(executionContext, targetPath, $"http.{repositoryUrl.AbsoluteUri}.extraheader")) @@ -1171,7 +1200,7 @@ public async Task GetSourceAsync( } } - int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken); + int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, additionalFetchFilterOptions, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken); if (exitCode_submoduleUpdate != 0) { throw new InvalidOperationException($"Git submodule update failed with exit code: {exitCode_submoduleUpdate}"); @@ -1444,7 +1473,7 @@ public async Task PostJobCleanupAsync(AgentTaskPluginExecutionContext executionC } } - public async void SetGitFeatureFlagsConfiguration( + public async Task SetGitFeatureFlagsConfiguration( AgentTaskPluginExecutionContext executionContext, IGitCliManager gitCommandManager, string targetPath) diff --git a/src/Agent.Plugins/PipelineCache/TarUtils.cs b/src/Agent.Plugins/PipelineCache/TarUtils.cs index 9ea9afea1b..4c6ee3591d 100644 --- a/src/Agent.Plugins/PipelineCache/TarUtils.cs +++ b/src/Agent.Plugins/PipelineCache/TarUtils.cs @@ -156,10 +156,13 @@ internal static async Task RunProcessAsync( } } - private static void CreateProcessStartInfo(ProcessStartInfo processStartInfo, string processFileName, string processArguments, string processWorkingDirectory) + private static void CreateProcessStartInfo(ProcessStartInfo processStartInfo, string processFileName, string[] processArguments, string processWorkingDirectory) { processStartInfo.FileName = processFileName; - processStartInfo.Arguments = processArguments; + foreach (var arg in processArguments) + { + processStartInfo.ArgumentList.Add(arg); + } processStartInfo.UseShellExecute = false; processStartInfo.RedirectStandardInput = true; processStartInfo.WorkingDirectory = processWorkingDirectory; @@ -169,19 +172,24 @@ private static ProcessStartInfo GetCreateTarProcessInfo(AgentTaskPluginExecution { var processFileName = GetTar(context); inputPath = inputPath.TrimEnd(Path.DirectorySeparatorChar).TrimEnd(Path.AltDirectorySeparatorChar); - var processArguments = $"-cf \"{archiveFileName}\" -C \"{inputPath}\" ."; // If given the absolute path for the '-cf' option, the GNU tar fails. The workaround is to start the tarring process in the temp directory, and simply speficy 'archive.tar' for that option. + var args = new System.Collections.Generic.List(); if (context.IsSystemDebugTrue()) { - processArguments = "-v " + processArguments; + args.Add("-v"); } if (isWindows) { - processArguments = "-h " + processArguments; + args.Add("-h"); } + args.Add("-cf"); + args.Add(archiveFileName); + args.Add("-C"); + args.Add(inputPath); + args.Add("."); ProcessStartInfo processStartInfo = new ProcessStartInfo(); - CreateProcessStartInfo(processStartInfo, processFileName, processArguments, processWorkingDirectory: Path.GetTempPath()); // We want to create the archiveFile in temp folder, and hence starting the tar process from TEMP to avoid absolute paths in tar cmd line. + CreateProcessStartInfo(processStartInfo, processFileName, args.ToArray(), processWorkingDirectory: Path.GetTempPath()); // We want to create the archiveFile in temp folder, and hence starting the tar process from TEMP to avoid absolute paths in tar cmd line. return processStartInfo; } @@ -194,28 +202,36 @@ private static string GetTar(AgentTaskPluginExecutionContext context) private static ProcessStartInfo GetExtractStartProcessInfo(AgentTaskPluginExecutionContext context, string targetDirectory) { - string processFileName, processArguments; + string processFileName; + var args = new System.Collections.Generic.List(); if (isWindows && CheckIf7ZExists()) { processFileName = "7z"; - processArguments = $"x -si -aoa -o\"{targetDirectory}\" -ttar"; if (context.IsSystemDebugTrue()) { - processArguments = "-bb1 " + processArguments; + args.Add("-bb1"); } + args.Add("x"); + args.Add("-si"); + args.Add("-aoa"); + args.Add($"-o{targetDirectory}"); + args.Add("-ttar"); } else { processFileName = GetTar(context); - processArguments = $"-xf - -C ."; // Instead of targetDirectory, we are providing . to tar, because the tar process is being started from targetDirectory. if (context.IsSystemDebugTrue()) { - processArguments = "-v " + processArguments; + args.Add("-v"); } + args.Add("-xf"); + args.Add("-"); + args.Add("-C"); + args.Add("."); // Instead of targetDirectory, we are providing . to tar, because the tar process is being started from targetDirectory. } ProcessStartInfo processStartInfo = new ProcessStartInfo(); - CreateProcessStartInfo(processStartInfo, processFileName, processArguments, processWorkingDirectory: targetDirectory); + CreateProcessStartInfo(processStartInfo, processFileName, args.ToArray(), processWorkingDirectory: targetDirectory); return processStartInfo; } diff --git a/src/Agent.Plugins/TFCliManager.cs b/src/Agent.Plugins/TFCliManager.cs index c6dced5bea..4a0bdb121b 100644 --- a/src/Agent.Plugins/TFCliManager.cs +++ b/src/Agent.Plugins/TFCliManager.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Agent.Sdk; +using Agent.Sdk.Util; using System; using System.Collections.Generic; using System.IO; @@ -160,7 +161,10 @@ public void SetupProxy(string proxyUrl, string proxyUsername, string proxyPasswo public void SetupClientCertificate(string clientCert, string clientCertKey, string clientCertArchive, string clientCertPassword) { ArgUtil.File(clientCert, nameof(clientCert)); - X509Certificate2 cert = new X509Certificate2(clientCert); + + // Pass null for password to maintain original behavior (certificate without password) + X509Certificate2 cert = CertificateUtil.LoadCertificate(clientCert, password: null); + ExecutionContext.Debug($"Set VstsClientCertificate={cert.Thumbprint} for Tf.exe to support client certificate."); AdditionalEnvironmentVariables["VstsClientCertificate"] = cert.Thumbprint; diff --git a/src/Agent.Sdk/Agent.Sdk.csproj b/src/Agent.Sdk/Agent.Sdk.csproj index dd8f45e512..484a9dabe1 100644 --- a/src/Agent.Sdk/Agent.Sdk.csproj +++ b/src/Agent.Sdk/Agent.Sdk.csproj @@ -8,21 +8,21 @@ - + - + - - - + + + - - + + \ No newline at end of file diff --git a/src/Agent.Sdk/AgentClientCertificateManager.cs b/src/Agent.Sdk/AgentClientCertificateManager.cs index 22a585201b..a368a38d73 100644 --- a/src/Agent.Sdk/AgentClientCertificateManager.cs +++ b/src/Agent.Sdk/AgentClientCertificateManager.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Security.Cryptography.X509Certificates; +using Agent.Sdk.Util; using Microsoft.VisualStudio.Services.Common; namespace Agent.Sdk @@ -35,7 +36,7 @@ public void AddClientCertificate(string clientCertificateArchiveFile, string cli { if (!string.IsNullOrEmpty(clientCertificateArchiveFile)) { - _clientCertificates.Add(new X509Certificate2(clientCertificateArchiveFile, clientCertificatePassword)); + _clientCertificates.Add(CertificateUtil.LoadCertificate(clientCertificateArchiveFile, clientCertificatePassword)); } } } diff --git a/src/Agent.Sdk/CommandPlugin.cs b/src/Agent.Sdk/CommandPlugin.cs index 4736ddf2fc..2e19fb9499 100644 --- a/src/Agent.Sdk/CommandPlugin.cs +++ b/src/Agent.Sdk/CommandPlugin.cs @@ -66,15 +66,11 @@ public void Info(string message, [CallerMemberName] string operation = "") public void Verbose(string message, [CallerMemberName] string operation = "") { -#if DEBUG - Debug(message); -#else string vstsAgentTrace = AgentKnobs.TraceVerbose.GetValue(this).AsString(); if (!string.IsNullOrEmpty(vstsAgentTrace)) { Debug(message); } -#endif } public void Debug(string message) diff --git a/src/Agent.Sdk/Knob/AgentKnobs.cs b/src/Agent.Sdk/Knob/AgentKnobs.cs index d834290e19..c4083524bc 100644 --- a/src/Agent.Sdk/Knob/AgentKnobs.cs +++ b/src/Agent.Sdk/Knob/AgentKnobs.cs @@ -231,6 +231,13 @@ public class AgentKnobs new EnvironmentKnobSource("VSTS_FETCHBYCOMMITFORFULLCLONE"), new BuiltInDefaultKnobSource("false")); + public static readonly Knob DisableAutoManagedVhdShallowOverride = new Knob( + nameof(DisableAutoManagedVhdShallowOverride), + "If true, the agent will NOT override shallow-fetch settings when an AutoManagedVHD full clone is detected.", + new RuntimeKnobSource("VSTS.DisableAutoManagedVhdShallowOverride"), + new EnvironmentKnobSource("VSTS_DISABLEAUTOMANAGEDVHD_SHALLOW_OVERRIDE"), + new BuiltInDefaultKnobSource("false")); + // Agent logging public static readonly Knob AgentPerflog = new Knob( nameof(AgentPerflog), @@ -339,6 +346,12 @@ public class AgentKnobs new EnvironmentKnobSource("VSTS_HTTP_RETRY"), new BuiltInDefaultKnobSource("3")); + public static readonly Knob EnableProgressiveRetryBackoff = new Knob( + nameof(EnableProgressiveRetryBackoff), + "If true, enables progressive backoff delays for agent message polling and keep-alive retries when encountering retriable errors", + new EnvironmentKnobSource("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF"), + new BuiltInDefaultKnobSource("false")); + public static readonly Knob HttpTimeout = new Knob( nameof(HttpTimeout), "Timeout for Http requests", @@ -561,6 +574,13 @@ public class AgentKnobs new EnvironmentKnobSource("AGENT_DISABLE_NODE6_TASKS"), new BuiltInDefaultKnobSource("false")); + public static readonly Knob EnableEOLNodeVersionPolicy = new Knob( + nameof(EnableEOLNodeVersionPolicy), + "When enabled, tasks that specify end-of-life Node.js versions (6, 10, 16) will run using a supported Node.js version available on the agent (Node 20.1 or Node 24), ignoring the EOL Node.js version(s) in respective task. An error is thrown if no supported version is available.", + new RuntimeKnobSource("AGENT_RESTRICT_EOL_NODE_VERSIONS"), + new EnvironmentKnobSource("AGENT_RESTRICT_EOL_NODE_VERSIONS"), + new BuiltInDefaultKnobSource("false")); + public static readonly Knob DisableTeePluginRemoval = new Knob( nameof(DisableTeePluginRemoval), "Disables removing TEE plugin after using it during checkout.", @@ -699,7 +719,7 @@ public class AgentKnobs "Ignores the VSTSTaskLib folder when copying tasks.", new RuntimeKnobSource("AZP_AGENT_IGNORE_VSTSTASKLIB"), new EnvironmentKnobSource("AZP_AGENT_IGNORE_VSTSTASKLIB"), - new BuiltInDefaultKnobSource("false")); + new BuiltInDefaultKnobSource("true")); public static readonly Knob FailJobWhenAgentDies = new Knob( nameof(FailJobWhenAgentDies), @@ -708,6 +728,12 @@ public class AgentKnobs new EnvironmentKnobSource("FAIL_JOB_WHEN_AGENT_DIES"), new BuiltInDefaultKnobSource("false")); + public static readonly Knob EnhancedWorkerCrashHandling = new Knob( + nameof(EnhancedWorkerCrashHandling), + "If true, enables enhanced worker crash handling with forced completion for Plan v8+ scenarios where worker crashes cannot send completion events", + new EnvironmentKnobSource("AZP_ENHANCED_WORKER_CRASH_HANDLING"), + new BuiltInDefaultKnobSource("false")); + public static readonly Knob AllowWorkDirectoryRepositories = new Knob( nameof(AllowWorkDirectoryRepositories), "Allows repositories to be checked out below work directory level on self hosted agents.", @@ -733,13 +759,15 @@ public class AgentKnobs "If true, the agent will use Node 20 to start docker container when executing container job and the container platform is the same as the host platform.", new PipelineFeatureSource("UseNode20ToStartContainer"), new RuntimeKnobSource("AZP_AGENT_USE_NODE20_TO_START_CONTAINER"), + new EnvironmentKnobSource("AZP_AGENT_USE_NODE20_TO_START_CONTAINER"), new BuiltInDefaultKnobSource("false")); public static readonly Knob UseNode24ToStartContainer = new Knob( nameof(UseNode24ToStartContainer), "If true, try to start container job using Node24, then fallback to Node20, then Node16.", - new RuntimeKnobSource("AZP_AGENT_USE_NODE24_TO_START_CONTAINER"), new PipelineFeatureSource("UseNode24ToStartContainer"), + new RuntimeKnobSource("AZP_AGENT_USE_NODE24_TO_START_CONTAINER"), + new EnvironmentKnobSource("AZP_AGENT_USE_NODE24_TO_START_CONTAINER"), new BuiltInDefaultKnobSource("false")); public static readonly Knob EnableNewMaskerAndRegexes = new Knob( @@ -783,6 +811,14 @@ public class AgentKnobs new RuntimeKnobSource("AGENT_USE_FETCH_FILTER_IN_CHECKOUT_TASK"), new BuiltInDefaultKnobSource("false")); + public static readonly Knob UseFetchFilterInGitSubmoduleUpdate = new Knob( + nameof(UseFetchFilterInGitSubmoduleUpdate), + "If true, agent will pass fetch filter options in checkout task to git submodule update.", + new PipelineFeatureSource("UseFetchFilterInGitSubmoduleUpdate"), + new RuntimeKnobSource("AGENT_USE_FETCH_FILTER_IN_GIT_SUBMODULE_UPDATE"), + new EnvironmentKnobSource("AGENT_USE_FETCH_FILTER_IN_GIT_SUBMODULE_UPDATE"), + new BuiltInDefaultKnobSource("false")); + public static readonly Knob StoreAgentKeyInCSPContainer = new Knob( nameof(StoreAgentKeyInCSPContainer), "Store agent key in named container (Windows).", @@ -836,6 +872,12 @@ public class AgentKnobs new PipelineFeatureSource("Net8UnsupportedOsWarning"), new BuiltInDefaultKnobSource("true")); + public static readonly Knob DisableUnsupportedOsWarningNet10 = new Knob( + nameof(DisableUnsupportedOsWarningNet10), + "Show warning message on the OS which is not supported by .NET 10", + new PipelineFeatureSource("DisableUnsupportedOsWarningNet10"), + new BuiltInDefaultKnobSource("true")); + public static readonly Knob UsePSScriptWrapper = new Knob( nameof(UsePSScriptWrapper), "Use PowerShell script wrapper to handle PowerShell ConstrainedLanguage mode.", @@ -921,5 +963,13 @@ public class AgentKnobs new PipelineFeatureSource("EnableDockerExecDiagnostics"), new EnvironmentKnobSource("AGENT_ENABLE_DOCKER_EXEC_DIAGNOSTICS"), new BuiltInDefaultKnobSource("false")); + + public static readonly Knob UseEnhancedNodeSelection = new Knob( + nameof(UseEnhancedNodeSelection), + "If true, use the enhanced Node.js version selection logic (both host and container). This provides centralized node selection with EOL policy enforcement and correct container keepalive. Set to false to use legacy node selection logic.", + new PipelineFeatureSource("UseEnhancedNodeSelection"), + new RuntimeKnobSource("AGENT_USE_ENHANCED_NODE_SELECTION"), + new EnvironmentKnobSource("AGENT_USE_ENHANCED_NODE_SELECTION"), + new BuiltInDefaultKnobSource("false")); } -} +} \ No newline at end of file diff --git a/src/Agent.Sdk/ProcessInvoker.cs b/src/Agent.Sdk/ProcessInvoker.cs index 07db939f40..204add976f 100644 --- a/src/Agent.Sdk/ProcessInvoker.cs +++ b/src/Agent.Sdk/ProcessInvoker.cs @@ -332,10 +332,20 @@ public async Task ExecuteAsync( AsyncManualResetEvent afterCancelKillProcessTreeAttemptSignal = new AsyncManualResetEvent(); using (var registration = cancellationToken.Register(async () => { - await CancelAndKillProcessTree(killProcessOnCancel); - - // signal to ensure we exit the loop after we attempt to cancel and kill the process tree (which is best effort) - afterCancelKillProcessTreeAttemptSignal.Set(); + try + { + await CancelAndKillProcessTree(killProcessOnCancel); + } + catch (Exception ex) + { + // Log the exception but continue with cleanup to prevent silent failures + Trace.Info($"Exception details: {ex}"); + } + finally + { + // signal to ensure we exit the loop after we attempt to cancel and kill the process tree (which is best effort) + afterCancelKillProcessTreeAttemptSignal.Set(); + } })) { Trace.Info($"Process started, waiting for process exit."); @@ -461,7 +471,7 @@ internal protected virtual async Task CancelAndKillProcessTree(bool killProcessO // Store proc reference locally to prevent race conditions with Dispose() ArgUtil.NotNull(_proc, nameof(_proc)); - + if (!killProcessOnCancel) { bool sigint_succeed = await SendSIGINT(SigintTimeout); diff --git a/src/Agent.Sdk/TaskPlugin.cs b/src/Agent.Sdk/TaskPlugin.cs index 43adfab4e8..16c53782cf 100644 --- a/src/Agent.Sdk/TaskPlugin.cs +++ b/src/Agent.Sdk/TaskPlugin.cs @@ -160,15 +160,11 @@ public void Info(string message, [CallerMemberName] string operation = "") public void Verbose(string message, [CallerMemberName] string operation = "") { ArgUtil.NotNull(message, nameof(message)); -#if DEBUG - Debug(message); -#else string vstsAgentTrace = AgentKnobs.TraceVerbose.GetValue(this).AsString(); if (!string.IsNullOrEmpty(vstsAgentTrace)) { Debug(message); } -#endif } public void Error(string message) diff --git a/src/Agent.Sdk/Util/CertificateUtil.cs b/src/Agent.Sdk/Util/CertificateUtil.cs new file mode 100644 index 0000000000..f3f3c82932 --- /dev/null +++ b/src/Agent.Sdk/Util/CertificateUtil.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Security.Cryptography.X509Certificates; + +namespace Agent.Sdk.Util +{ + public static class CertificateUtil + { + /// + /// Loads an X509Certificate2 from a file, handling different certificate formats. + /// Uses X509CertificateLoader for .NET 9+ for Cert and Pkcs12 formats. + /// For all other formats, uses the legacy constructor with warning suppression. + /// + /// Path to the certificate file + /// Optional password for PKCS#12/PFX files + /// The loaded X509Certificate2 + public static X509Certificate2 LoadCertificate(string certificatePath, string password = null) + { +#if NET9_0_OR_GREATER + var contentType = X509Certificate2.GetCertContentType(certificatePath); + switch (contentType) + { + case X509ContentType.Cert: + // DER-encoded or PEM-encoded certificate + return X509CertificateLoader.LoadCertificateFromFile(certificatePath); + + case X509ContentType.Pkcs12: + // Note: X509ContentType.Pfx has the same value (3) as Pkcs12 refer: https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509contenttype?view=net-10.0 + return X509CertificateLoader.LoadPkcs12FromFile(certificatePath, password); + + default: + // For all other formats (Pkcs7, SerializedCert, SerializedStore, Authenticode, Unknown), + // use the legacy constructor with warning suppression +#pragma warning disable SYSLIB0057 + if (string.IsNullOrEmpty(password)) + { + return new X509Certificate2(certificatePath); + } + else + { + return new X509Certificate2(certificatePath, password); + } +#pragma warning restore SYSLIB0057 + } +#else + // For .NET 8 and earlier, use the traditional constructor + // The constructor automatically handles all certificate types + if (string.IsNullOrEmpty(password)) + { + return new X509Certificate2(certificatePath); + } + else + { + return new X509Certificate2(certificatePath, password); + } +#endif + } + } +} diff --git a/src/Agent.Sdk/Util/IOUtil.cs b/src/Agent.Sdk/Util/IOUtil.cs index 4b868eca5b..6c8288faa3 100644 --- a/src/Agent.Sdk/Util/IOUtil.cs +++ b/src/Agent.Sdk/Util/IOUtil.cs @@ -569,6 +569,30 @@ private static void RemoveReadOnly(FileSystemInfo item) } } + /// + /// Attempts to create a directory. Returns true if created or already exists, false on failure. + /// Does NOT throw — callers decide how to handle failure. + /// + public static bool TryCreateDirectory(string path) + { + try + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + return true; + } + catch (UnauthorizedAccessException) + { + return false; + } + catch (IOException) + { + return false; + } + } + public static string GetDirectoryName(string path, PlatformUtil.OS platform) { if (string.IsNullOrWhiteSpace(path)) diff --git a/src/Agent.Sdk/Util/TeeUtil.cs b/src/Agent.Sdk/Util/TeeUtil.cs index 73c99537cd..d94dd83502 100644 --- a/src/Agent.Sdk/Util/TeeUtil.cs +++ b/src/Agent.Sdk/Util/TeeUtil.cs @@ -123,10 +123,15 @@ private void SetPermissions(string path, string permissions, bool recursive = fa { var chmodProcessInfo = new ProcessStartInfo("chmod") { - Arguments = $"{(recursive ? "-R" : "")} {permissions} {path}", UseShellExecute = false, RedirectStandardError = true }; + if (recursive) + { + chmodProcessInfo.ArgumentList.Add("-R"); + } + chmodProcessInfo.ArgumentList.Add(permissions); + chmodProcessInfo.ArgumentList.Add(path); Process chmodProcess = Process.Start(chmodProcessInfo); chmodProcess.WaitForExit(); diff --git a/src/Agent.Worker/Agent.Worker.csproj b/src/Agent.Worker/Agent.Worker.csproj index 1beec9b6c7..78ef4d299d 100644 --- a/src/Agent.Worker/Agent.Worker.csproj +++ b/src/Agent.Worker/Agent.Worker.csproj @@ -16,10 +16,10 @@ - + - - - + + + diff --git a/src/Agent.Worker/AssemblyInfo.cs b/src/Agent.Worker/AssemblyInfo.cs new file mode 100644 index 0000000000..0012a29492 --- /dev/null +++ b/src/Agent.Worker/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Test")] diff --git a/src/Agent.Worker/Build/BuildCommandExtension.cs b/src/Agent.Worker/Build/BuildCommandExtension.cs index d068e04e10..c18fdbad90 100644 --- a/src/Agent.Worker/Build/BuildCommandExtension.cs +++ b/src/Agent.Worker/Build/BuildCommandExtension.cs @@ -147,7 +147,7 @@ public void Execute(IExecutionContext context, Command command) ArgUtil.NotNull(context.Endpoints, nameof(context.Endpoints)); ArgUtil.NotNull(command, nameof(command)); - string data = command.Data; + string data = command.Data?.Trim(); Guid projectId = context.Variables.System_TeamProjectId ?? Guid.Empty; ArgUtil.NotEmpty(projectId, nameof(projectId)); diff --git a/src/Agent.Worker/Build/GitSourceProvider.cs b/src/Agent.Worker/Build/GitSourceProvider.cs index c57c660e3a..9de59af237 100644 --- a/src/Agent.Worker/Build/GitSourceProvider.cs +++ b/src/Agent.Worker/Build/GitSourceProvider.cs @@ -666,7 +666,7 @@ public async Task GetSourceAsync( executionContext.Warning($"Forcing Git to HTTP/1.1 failed with exit code: {exitCode_configHttp}"); } - SetGitFeatureFlagsConfiguration(executionContext, _gitCommandManager, targetPath); + await SetGitFeatureFlagsConfiguration(executionContext, _gitCommandManager, targetPath); // always remove any possible left extraheader setting from git config. if (await _gitCommandManager.GitConfigExist(executionContext, targetPath, $"http.{repositoryUrl.AbsoluteUri}.extraheader")) @@ -1270,7 +1270,7 @@ public override async Task RunMaintenanceOperations(IExecutionContext executionC } } - public async void SetGitFeatureFlagsConfiguration( + public async Task SetGitFeatureFlagsConfiguration( IExecutionContext executionContext, IGitCommandManager gitCommandManager, string targetPath) diff --git a/src/Agent.Worker/Build/TFCommandManager.cs b/src/Agent.Worker/Build/TFCommandManager.cs index 4a10f2e202..e386c55478 100644 --- a/src/Agent.Worker/Build/TFCommandManager.cs +++ b/src/Agent.Worker/Build/TFCommandManager.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Agent.Sdk.Util; using Microsoft.VisualStudio.Services.Agent.Util; using System; using System.Collections.Generic; @@ -157,7 +158,10 @@ public void SetupProxy(string proxyUrl, string proxyUsername, string proxyPasswo public void SetupClientCertificate(string clientCert, string clientCertKey, string clientCertArchive, string clientCertPassword) { ArgUtil.File(clientCert, nameof(clientCert)); - X509Certificate2 cert = new X509Certificate2(clientCert); + + // Pass null for password to maintain original behavior (certificate without password) + X509Certificate2 cert = CertificateUtil.LoadCertificate(clientCert, password: null); + ExecutionContext.Debug($"Set VstsClientCertificate={cert.Thumbprint} for Tf.exe to support client certificate."); AdditionalEnvironmentVariables["VstsClientCertificate"] = cert.Thumbprint; diff --git a/src/Agent.Worker/ContainerOperationProvider.cs b/src/Agent.Worker/ContainerOperationProvider.cs index 0ddc5a28a2..7f9f59c5bf 100644 --- a/src/Agent.Worker/ContainerOperationProvider.cs +++ b/src/Agent.Worker/ContainerOperationProvider.cs @@ -10,6 +10,7 @@ using Microsoft.VisualStudio.Services.Agent.Util; using Microsoft.VisualStudio.Services.Agent.Worker.Container; using Microsoft.VisualStudio.Services.Agent.Worker.Handlers; +using Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies; using Microsoft.VisualStudio.Services.Agent.Worker.Telemetry; using Microsoft.VisualStudio.Services.Common; using Microsoft.VisualStudio.Services.WebApi; @@ -222,17 +223,33 @@ private async Task GetAccessTokenUsingWorkloadIdentityFederation(IExecut using VssConnection vssConnection = VssUtil.CreateConnection(collectionUri, vssCredentials, trace: Trace); TaskHttpClient taskClient = vssConnection.GetClient(); - var idToken = await taskClient.CreateOidcTokenAsync( - scopeIdentifier: executionContext.Variables.System_TeamProjectId ?? throw new ArgumentException("Unknown team Project ID"), - hubName: Enum.GetName(typeof(HostTypes), executionContext.Variables.System_HostType), - planId: new Guid(executionContext.Variables.System_PlanId), - jobId: new Guid(executionContext.Variables.System_JobId), - serviceConnectionId: registryEndpoint.Id, - claims: null, - cancellationToken: cancellationToken - ); - - return idToken.OidcToken; + const int maxRetries = 3; + + for (int attempt = 1; attempt <= maxRetries + 1; attempt++) + { + try + { + var idToken = await taskClient.CreateOidcTokenAsync( + scopeIdentifier: executionContext.Variables.System_TeamProjectId ?? throw new ArgumentException("Unknown team Project ID"), + hubName: Enum.GetName(typeof(HostTypes), executionContext.Variables.System_HostType), + planId: new Guid(executionContext.Variables.System_PlanId), + jobId: new Guid(executionContext.Variables.System_JobId), + serviceConnectionId: registryEndpoint.Id, + claims: null, + cancellationToken: cancellationToken + ); + Trace.Info("OIDC token created successfully"); + return idToken.OidcToken; + } + catch (TaskOrchestrationPlanSecurityException ex) when (attempt <= maxRetries) + { + TimeSpan backoff = TimeSpan.FromSeconds(Math.Pow(5, attempt - 1)); + executionContext.Debug($"Failed to acquire OIDC token(attempt {attempt}/{maxRetries}): {ex.Message}. Retrying in {backoff.TotalSeconds} seconds..."); + await Task.Delay(backoff, cancellationToken); + } + } + + throw new InvalidOperationException("Failed to acquire OIDC token after all retry attempts."); }) .Build(); @@ -440,9 +457,17 @@ private async Task PullContainerAsync(IExecutionContext executionContext, Contai string containerOS = await _dockerManger.DockerInspect(context: executionContext, dockerObject: container.ContainerImage, options: $"--format=\"{{{{.Os}}}}\""); + executionContext.Debug($"[Container OS Detection] Detected container OS: {containerOS}"); + if (string.Equals("linux", containerOS, StringComparison.OrdinalIgnoreCase)) { container.ImageOS = PlatformUtil.OS.Linux; + executionContext.Debug("[Container OS Detection] Set container ImageOS to Linux"); + } + else if (string.Equals("windows", containerOS, StringComparison.OrdinalIgnoreCase)) + { + container.ImageOS = PlatformUtil.OS.Windows; + executionContext.Debug("[Container OS Detection] Set container ImageOS to Windows"); } } } @@ -468,6 +493,8 @@ private async Task StartContainerAsync(IExecutionContext executionContext, Conta ArgUtil.NotNull(container, nameof(container)); ArgUtil.NotNullOrEmpty(container.ContainerImage, nameof(container.ContainerImage)); + executionContext.Debug($"Starting container: {container.ContainerName} (Image: {container.ContainerImage})"); + Trace.Info($"Container name: {container.ContainerName}"); Trace.Info($"Container image: {container.ContainerImage}"); Trace.Info($"Container registry: {container.ContainerRegistryEndpoint.ToString()}"); @@ -526,6 +553,7 @@ private async Task StartContainerAsync(IExecutionContext executionContext, Conta container.MountVolumes.Add(new MountVolume(taskKeyFile, container.TranslateToContainerPath(taskKeyFile))); } + bool useEnhancedNodeSelection = AgentKnobs.UseEnhancedNodeSelection.GetValue(executionContext).AsBoolean(); bool useNode20ToStartContainer = AgentKnobs.UseNode20ToStartContainer.GetValue(executionContext).AsBoolean(); bool useNode24ToStartContainer = AgentKnobs.UseNode24ToStartContainer.GetValue(executionContext).AsBoolean(); bool useAgentNode = false; @@ -545,65 +573,86 @@ string containerNodePath(string nodeFolder) string node20ContainerPath = containerNodePath(NodeHandler.Node20_1Folder); string node24ContainerPath = containerNodePath(NodeHandler.Node24Folder); + if (container.IsJobContainer) { - // See if this container brings its own Node.js + // Check if this container brings its own Node.js (needed for both strategies) container.CustomNodePath = await _dockerManger.DockerInspect(context: executionContext, dockerObject: container.ContainerImage, options: $"--format=\"{{{{index .Config.Labels \\\"{_nodeJsPathLabel}\\\"}}}}\""); - string nodeSetInterval(string node) - { - return $"'{node}' -e 'setInterval(function(){{}}, 24 * 60 * 60 * 1000);'"; - } - - string useDoubleQuotes(string value) - { - return value.Replace('\'', '"'); - } - - if (!string.IsNullOrEmpty(container.CustomNodePath)) - { - container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath)); - container.ResultNodePath = container.CustomNodePath; - } - else if (PlatformUtil.RunningOnMacOS || (PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux)) + if (useEnhancedNodeSelection) { - // require container to have node if running on macOS, or if running on Windows and attempting to run Linux container - container.CustomNodePath = "node"; - container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath)); - container.ResultNodePath = container.CustomNodePath; + executionContext.Debug("[ContainerSetup] Using enhanced node selection path for container startup."); + bool isWindowsContainer = container.ImageOS == PlatformUtil.OS.Windows; + + container.ContainerCommand = isWindowsContainer + ? "cmd.exe /c ping -t localhost > nul" + : "sleep infinity"; } else { - useAgentNode = true; - string sleepCommand; + executionContext.Debug("[ContainerSetup] Using legacy node selection path for container startup."); + // Legacy approach: Use node-based startup command + string nodeSetInterval(string node) + { + return $"'{node}' -e 'setInterval(function(){{}}, 24 * 60 * 60 * 1000);'"; + } - if (useNode24ToStartContainer) + string useDoubleQuotes(string value) { - sleepCommand = $"'{node24ContainerPath}' --version && echo '{labelContainerStartupUsingNode24}' && {nodeSetInterval(node24ContainerPath)} || '{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'"; + return value.Replace('\'', '"'); } - else if (useNode20ToStartContainer) + + if (!string.IsNullOrEmpty(container.CustomNodePath)) + { + executionContext.Debug($"[ContainerSetup] Legacy path: Using container's custom node: {container.CustomNodePath}"); + container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath)); + container.ResultNodePath = container.CustomNodePath; + } + else if (PlatformUtil.RunningOnMacOS || (PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux)) { - sleepCommand = $"'{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'"; + // require container to have node if running on macOS, or if running on Windows and attempting to run Linux container + executionContext.Debug($"[ContainerSetup] Legacy path: Platform requirement - using container node. MacOS: {PlatformUtil.RunningOnMacOS}, Windows+LinuxContainer: {PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux}"); + container.CustomNodePath = "node"; + container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath)); + container.ResultNodePath = container.CustomNodePath; } else { - sleepCommand = nodeSetInterval(nodeContainerPath); + executionContext.Debug("[ContainerSetup] Legacy path: Using agent node with fallback strategy"); + useAgentNode = true; + string sleepCommand; + + if (useNode24ToStartContainer) + { + executionContext.Debug("[ContainerSetup] Legacy agent node: Using Node24 with fallbacks (24->20->16)"); + sleepCommand = $"'{node24ContainerPath}' --version && echo '{labelContainerStartupUsingNode24}' && {nodeSetInterval(node24ContainerPath)} || '{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'"; + } + else if (useNode20ToStartContainer) + { + executionContext.Debug("[ContainerSetup] Legacy agent node: Using Node20 with fallbacks (20->16)"); + sleepCommand = $"'{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'"; + } + else + { + executionContext.Debug($"[ContainerSetup] Legacy agent node: Using default node path: {nodeContainerPath}"); + sleepCommand = nodeSetInterval(nodeContainerPath); + } + container.ContainerCommand = PlatformUtil.RunningOnWindows ? $"cmd.exe /c call {useDoubleQuotes(sleepCommand)}" : $"bash -c \"{sleepCommand}\""; + container.ResultNodePath = nodeContainerPath; } - container.ContainerCommand = PlatformUtil.RunningOnWindows ? $"cmd.exe /c call {useDoubleQuotes(sleepCommand)}" : $"bash -c \"{sleepCommand}\""; - container.ResultNodePath = nodeContainerPath; } } container.ContainerId = await _dockerManger.DockerCreate(executionContext, container); ArgUtil.NotNullOrEmpty(container.ContainerId, nameof(container.ContainerId)); + if (container.IsJobContainer) { executionContext.Variables.Set(Constants.Variables.Agent.ContainerId, container.ContainerId); } - // Start container int startExitCode = await _dockerManger.DockerStart(executionContext, container.ContainerId); if (startExitCode != 0) { @@ -626,6 +675,20 @@ string useDoubleQuotes(string value) executionContext.Warning($"Docker container {container.ContainerId} is not in running state."); } + else if (useEnhancedNodeSelection && container.IsJobContainer) + { + try + { + executionContext.Debug("[ContainerSetup] Calling enhanced node selection path for container startup."); + SetContainerNodePathWithOrchestrator(executionContext, container); + } + catch (Exception ex) + { + executionContext.Error($"Failed to determine node path with orchestrator: {ex.Message}"); + container.ResultNodePath = !string.IsNullOrEmpty(container.CustomNodePath) ? container.CustomNodePath : nodeContainerPath; + executionContext.Warning($"Using fallback node path: {container.ResultNodePath}"); + } + } else if (useAgentNode && (useNode20ToStartContainer || useNode24ToStartContainer)) { bool containerStartupCompleted = false; @@ -641,28 +704,36 @@ string useDoubleQuotes(string value) { if (logLine.Contains(labelContainerStartupUsingNode24)) { - executionContext.Debug("Using Node 24 for container startup."); containerStartupCompleted = true; container.ResultNodePath = node24ContainerPath; break; } else if (logLine.Contains(labelContainerStartupUsingNode20)) { - executionContext.Debug("Using Node 20 for container startup."); + string warningMsg = useNode24ToStartContainer + ? "Cannot run Node 24 in container. Falling back to Node 20 for container startup." + : "Using Node 20 for container startup."; + executionContext.Warning(warningMsg); containerStartupCompleted = true; container.ResultNodePath = node20ContainerPath; break; } else if (logLine.Contains(labelContainerStartupUsingNode16)) { - executionContext.Warning("Can not run Node 20 in container. Falling back to Node 16 for container startup."); + string warningMsg = useNode24ToStartContainer + ? "Cannot run Node 24 and Node 20 in container. Falling back to Node 16 for container startup." + : "Cannot run Node 20 in container. Falling back to Node 16 for container startup."; + executionContext.Warning(warningMsg); containerStartupCompleted = true; container.ResultNodePath = node16ContainerPath; break; } else if (logLine.Contains(labelContainerStartupFailed)) { - executionContext.Error("Can not run both Node 20 and Node 16 in container. Container startup failed."); + string errorMsg = useNode24ToStartContainer + ? "Cannot run Node 24, Node 20, and Node 16 in container. Container startup failed." + : "Cannot run both Node 20 and Node 16 in container. Container startup failed."; + executionContext.Error(errorMsg); containerStartupCompleted = true; break; } @@ -676,7 +747,7 @@ string useDoubleQuotes(string value) checksCount++; if (checksCount * delayInMilliseconds > containerStartupTimeoutInMilliseconds) { - executionContext.Warning("Can not get startup status from container."); + executionContext.Warning($"Container startup timeout after {checksCount * delayInMilliseconds}ms. Cannot get startup status from container."); break; } @@ -999,19 +1070,6 @@ string useDoubleQuotes(string value) } } - if (!container.NeedsNode20Redirect) - { - container.ResultNodePath = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node24Folder, "bin", $"node{IOUtil.ExeExtension}")); - } - else if (!container.NeedsNode16Redirect) - { - container.ResultNodePath = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node20_1Folder, "bin", $"node{IOUtil.ExeExtension}")); - } - else - { - container.ResultNodePath = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node16Folder, "bin", $"node{IOUtil.ExeExtension}")); - } - } if (!string.IsNullOrEmpty(containerUserName)) @@ -1020,6 +1078,8 @@ string useDoubleQuotes(string value) } } } + + executionContext.Output($"Container setup complete: {container.ContainerName}"); } private async Task StopContainerAsync(IExecutionContext executionContext, ContainerInfo container) @@ -1250,6 +1310,71 @@ private static void ThrowIfWrongWindowsVersion(IExecutionContext executionContex } } + /// + /// Creates appropriate handler data for container job based on knobs and custom node path. + /// Used by orchestrator to determine the best node version for the container. + /// + private BaseNodeHandlerData GetJobContainerHandlerData(IExecutionContext executionContext, ContainerInfo container) + { + // Custom node takes highest priority + if (!string.IsNullOrEmpty(container.CustomNodePath)) + { + return new CustomNodeHandlerData(); + } + + // Special platform requirement: macOS or Windows with Linux containers must use container's own node + if (PlatformUtil.RunningOnMacOS || (PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux)) + { + container.CustomNodePath = "node"; + return new CustomNodeHandlerData(); + } + + // Check knobs to determine default handler preference + bool useNode24 = AgentKnobs.UseNode24ToStartContainer.GetValue(executionContext).AsBoolean(); + bool useNode20 = AgentKnobs.UseNode20ToStartContainer.GetValue(executionContext).AsBoolean(); + + if (useNode24) + { + return new Node24HandlerData(); + } + else if (useNode20) + { + return new Node20_1HandlerData(); + } + else + { + return new Node20_1HandlerData(); + } + } + + /// + /// Uses the NodeVersionOrchestrator to determine the optimal node version for the container. + /// Sets container.ResultNodePath based on orchestrator decision. + /// + private void SetContainerNodePathWithOrchestrator(IExecutionContext executionContext, ContainerInfo container) + { + var handlerData = GetJobContainerHandlerData(executionContext, container); + + var taskContext = new NodeVersionStrategies.TaskContext + { + HandlerData = handlerData, + Container = container, + StepTarget = null + }; + + var orchestrator = new NodeVersionStrategies.NodeVersionOrchestrator(executionContext, HostContext); + var result = orchestrator.SelectNodeVersionForContainer(taskContext, _dockerManger); + + container.ResultNodePath = result.NodePath; + + if (!string.IsNullOrEmpty(result.Warning)) + { + executionContext.Warning(result.Warning); + } + + executionContext.Output($"Container node selection: {result.NodeVersion} - {result.Reason}"); + } + private void PublishTelemetry( IExecutionContext executionContext, object telemetryData, diff --git a/src/Agent.Worker/ContainerOperationProviderEnhanced.cs b/src/Agent.Worker/ContainerOperationProviderEnhanced.cs index 2eb9826676..e01045aa37 100644 --- a/src/Agent.Worker/ContainerOperationProviderEnhanced.cs +++ b/src/Agent.Worker/ContainerOperationProviderEnhanced.cs @@ -242,18 +242,33 @@ private async Task GetAccessTokenUsingWorkloadIdentityFederation(IExecut using VssConnection vssConnection = VssUtil.CreateConnection(collectionUri, vssCredentials, trace: Trace); TaskHttpClient taskClient = vssConnection.GetClient(); - var idToken = await taskClient.CreateOidcTokenAsync( - scopeIdentifier: executionContext.Variables.System_TeamProjectId ?? throw new ArgumentException("Unknown team Project ID"), - hubName: Enum.GetName(typeof(HostTypes), executionContext.Variables.System_HostType), - planId: new Guid(executionContext.Variables.System_PlanId), - jobId: new Guid(executionContext.Variables.System_JobId), - serviceConnectionId: registryEndpoint.Id, - claims: null, - cancellationToken: cancellationToken - ); - - trace.Info("OIDC token created successfully"); - return idToken.OidcToken; + const int maxRetries = 3; + + for (int attempt = 1; attempt <= maxRetries + 1; attempt++) + { + try + { + var idToken = await taskClient.CreateOidcTokenAsync( + scopeIdentifier: executionContext.Variables.System_TeamProjectId ?? throw new ArgumentException("Unknown team Project ID"), + hubName: Enum.GetName(typeof(HostTypes), executionContext.Variables.System_HostType), + planId: new Guid(executionContext.Variables.System_PlanId), + jobId: new Guid(executionContext.Variables.System_JobId), + serviceConnectionId: registryEndpoint.Id, + claims: null, + cancellationToken: cancellationToken + ); + trace.Info("OIDC token created successfully"); + return idToken.OidcToken; + } + catch (TaskOrchestrationPlanSecurityException ex) when (attempt <= maxRetries) + { + TimeSpan backoff = TimeSpan.FromSeconds(Math.Pow(5, attempt - 1)); + executionContext.Debug($"Failed to acquire OIDC token(attempt {attempt}/{maxRetries}): {ex.Message}. Retrying in {backoff.TotalSeconds} seconds..."); + await Task.Delay(backoff, cancellationToken); + } + } + + throw new InvalidOperationException("Failed to acquire OIDC token after all retry attempts."); }) .Build(); @@ -647,9 +662,12 @@ private async Task StartContainerAsync(IExecutionContext executionContext, Conta $"{m.SourceVolumePath ?? "anonymous"}:{m.TargetVolumePath}{(m.ReadOnly ? ":ro" : "")}"); trace.Info($"Configured {container.MountVolumes.Count} volume mounts: {string.Join(", ", mountSummary)}"); + bool useEnhancedNodeSelection = AgentKnobs.UseEnhancedNodeSelection.GetValue(executionContext).AsBoolean(); bool useNode20ToStartContainer = AgentKnobs.UseNode20ToStartContainer.GetValue(executionContext).AsBoolean(); + bool useNode24ToStartContainer = AgentKnobs.UseNode24ToStartContainer.GetValue(executionContext).AsBoolean(); bool useAgentNode = false; + string labelContainerStartupUsingNode24 = "container-startup-using-node-24"; string labelContainerStartupUsingNode20 = "container-startup-using-node-20"; string labelContainerStartupUsingNode16 = "container-startup-using-node-16"; string labelContainerStartupFailed = "container-startup-failed"; @@ -662,6 +680,7 @@ string containerNodePath(string nodeFolder) string nodeContainerPath = containerNodePath(NodeHandler.NodeFolder); string node16ContainerPath = containerNodePath(NodeHandler.Node16Folder); string node20ContainerPath = containerNodePath(NodeHandler.Node20_1Folder); + string node24ContainerPath = containerNodePath(NodeHandler.Node24Folder); if (container.IsJobContainer) { @@ -671,37 +690,71 @@ string containerNodePath(string nodeFolder) dockerObject: container.ContainerImage, options: $"--format=\"{{{{index .Config.Labels \\\"{_nodeJsPathLabel}\\\"}}}}\""); - string nodeSetInterval(string node) - { - return $"'{node}' -e 'setInterval(function(){{}}, 24 * 60 * 60 * 1000);'"; - } - - string useDoubleQuotes(string value) + if (useEnhancedNodeSelection) { - return value.Replace('\'', '"'); - } - - if (!string.IsNullOrEmpty(container.CustomNodePath)) - { - trace.Info($"Container provides custom Node.js at: {container.CustomNodePath}"); - container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath)); - container.ResultNodePath = container.CustomNodePath; - } - else if (PlatformUtil.RunningOnMacOS || (PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux)) - { - trace.Info("Platform requires container to provide Node.js, using 'node' command"); - container.CustomNodePath = "node"; - container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath)); - container.ResultNodePath = container.CustomNodePath; + executionContext.Debug("[ContainerSetup] Using enhanced node selection path for container startup."); + bool isWindowsContainer = container.ImageOS == PlatformUtil.OS.Windows; + + container.ContainerCommand = isWindowsContainer + ? "cmd.exe /c ping -t localhost > nul" + : "sleep infinity"; } else { - useAgentNode = true; - trace.Info($"Using agent-provided Node.js. Node20 enabled: {useNode20ToStartContainer}"); - trace.Info($"Node paths - Default: {nodeContainerPath}, Node16: {node16ContainerPath}, Node20: {node20ContainerPath}"); - string sleepCommand = useNode20ToStartContainer ? $"'{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'" : nodeSetInterval(nodeContainerPath); - container.ContainerCommand = PlatformUtil.RunningOnWindows ? $"cmd.exe /c call {useDoubleQuotes(sleepCommand)}" : $"bash -c \"{sleepCommand}\""; - container.ResultNodePath = nodeContainerPath; + executionContext.Debug("[ContainerSetup] Using legacy node selection path for container startup."); + // Legacy approach: Use node-based startup command + string nodeSetInterval(string node) + { + return $"'{node}' -e 'setInterval(function(){{}}, 24 * 60 * 60 * 1000);'"; + } + + string useDoubleQuotes(string value) + { + return value.Replace('\'', '"'); + } + + if (!string.IsNullOrEmpty(container.CustomNodePath)) + { + trace.Info($"Container provides custom Node.js at: {container.CustomNodePath}"); + executionContext.Debug($"[ContainerSetup] Legacy path: Using container's custom node: {container.CustomNodePath}"); + container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath)); + container.ResultNodePath = container.CustomNodePath; + } + else if (PlatformUtil.RunningOnMacOS || (PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux)) + { + trace.Info("Platform requires container to provide Node.js, using 'node' command"); + // require container to have node if running on macOS, or if running on Windows and attempting to run Linux container + executionContext.Debug($"[ContainerSetup] Legacy path: Platform requirement - using container node. MacOS: {PlatformUtil.RunningOnMacOS}, Windows+LinuxContainer: {PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux}"); + container.CustomNodePath = "node"; + container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath)); + container.ResultNodePath = container.CustomNodePath; + } + else + { + executionContext.Debug("[ContainerSetup] Legacy path: Using agent node with fallback strategy"); + useAgentNode = true; + trace.Info($"Using agent-provided Node.js. Node20 enabled: {useNode20ToStartContainer}, Node24 enabled: {useNode24ToStartContainer}"); + trace.Info($"Node paths - Default: {nodeContainerPath}, Node16: {node16ContainerPath}, Node20: {node20ContainerPath}, Node24: {node24ContainerPath}"); + string sleepCommand; + + if (useNode24ToStartContainer) + { + executionContext.Debug("[ContainerSetup] Legacy agent node: Using Node24 with fallbacks (24->20->16)"); + sleepCommand = $"'{node24ContainerPath}' --version && echo '{labelContainerStartupUsingNode24}' && {nodeSetInterval(node24ContainerPath)} || '{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'"; + } + else if (useNode20ToStartContainer) + { + executionContext.Debug("[ContainerSetup] Legacy agent node: Using Node20 with fallbacks (20->16)"); + sleepCommand = $"'{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'"; + } + else + { + executionContext.Debug($"[ContainerSetup] Legacy agent node: Using default node path: {nodeContainerPath}"); + sleepCommand = nodeSetInterval(nodeContainerPath); + } + container.ContainerCommand = PlatformUtil.RunningOnWindows ? $"cmd.exe /c call {useDoubleQuotes(sleepCommand)}" : $"bash -c \"{sleepCommand}\""; + container.ResultNodePath = nodeContainerPath; + } } } @@ -762,7 +815,21 @@ string useDoubleQuotes(string value) executionContext.Warning($"Docker container {container.ContainerId} is not in running state."); } - else if (useAgentNode && useNode20ToStartContainer) + else if (useEnhancedNodeSelection && container.IsJobContainer) + { + try + { + executionContext.Debug("[ContainerSetup] Calling enhanced node selection path for container startup."); + SetContainerNodePathWithOrchestrator(executionContext, container); + } + catch (Exception ex) + { + executionContext.Error($"Failed to determine node path with orchestrator: {ex.Message}"); + container.ResultNodePath = !string.IsNullOrEmpty(container.CustomNodePath) ? container.CustomNodePath : nodeContainerPath; + executionContext.Warning($"Using fallback node path: {container.ResultNodePath}"); + } + } + else if (useAgentNode && (useNode20ToStartContainer || useNode24ToStartContainer)) { bool containerStartupCompleted = false; int containerStartupTimeoutInMilliseconds = 10000; @@ -777,23 +844,39 @@ string useDoubleQuotes(string value) foreach (string logLine in containerLogs) { - if (logLine.Contains(labelContainerStartupUsingNode20)) + if (logLine.Contains(labelContainerStartupUsingNode24)) { - executionContext.Debug("Using Node 20 for container startup."); + executionContext.Debug("Using Node 24 for container startup."); + containerStartupCompleted = true; + container.ResultNodePath = node24ContainerPath; + break; + } + else if (logLine.Contains(labelContainerStartupUsingNode20)) + { + string warningMsg = useNode24ToStartContainer + ? "Cannot run Node 24 in container. Falling back to Node 20 for container startup." + : "Using Node 20 for container startup."; + executionContext.Warning(warningMsg); containerStartupCompleted = true; container.ResultNodePath = node20ContainerPath; break; } else if (logLine.Contains(labelContainerStartupUsingNode16)) { - executionContext.Warning("Can not run Node 20 in container. Falling back to Node 16 for container startup."); + string warningMsg = useNode24ToStartContainer + ? "Cannot run Node 24 and Node 20 in container. Falling back to Node 16 for container startup." + : "Cannot run Node 20 in container. Falling back to Node 16 for container startup."; + executionContext.Warning(warningMsg); containerStartupCompleted = true; container.ResultNodePath = node16ContainerPath; break; } else if (logLine.Contains(labelContainerStartupFailed)) { - executionContext.Error("Can not run both Node 20 and Node 16 in container. Container startup failed."); + string errorMsg = useNode24ToStartContainer + ? "Cannot run Node 24, Node 20, and Node 16 in container. Container startup failed." + : "Cannot run both Node 20 and Node 16 in container. Container startup failed."; + executionContext.Error(errorMsg); containerStartupCompleted = true; break; } @@ -1114,8 +1197,30 @@ string useDoubleQuotes(string value) if (PlatformUtil.RunningOnLinux) { bool useNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(executionContext).AsBoolean(); + bool useNode24InUnsupportedSystem = AgentKnobs.UseNode24InUnsupportedSystem.GetValue(executionContext).AsBoolean(); + + if (!useNode24InUnsupportedSystem) + { + var node24 = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node24Folder, "bin", $"node{IOUtil.ExeExtension}")); + + string node24TestCmd = $"bash -c \"{node24} -v\""; + List node24VersionOutput = await DockerExec(executionContext, container.ContainerId, node24TestCmd, noExceptionOnError: true); - if (!useNode20InUnsupportedSystem) + container.NeedsNode20Redirect = WorkerUtilities.IsCommandResultGlibcError(executionContext, node24VersionOutput, out string node24InfoLine); + + if (container.NeedsNode20Redirect) + { + PublishTelemetry( + executionContext, + new Dictionary + { + { "ContainerNode24to20Fallback", container.NeedsNode20Redirect.ToString() } + } + ); + } + } + + if (!useNode20InUnsupportedSystem && (useNode24InUnsupportedSystem || container.NeedsNode20Redirect)) { var node20 = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node20_1Folder, "bin", $"node{IOUtil.ExeExtension}")); @@ -1130,12 +1235,11 @@ string useDoubleQuotes(string value) executionContext, new Dictionary { - { "ContainerNode20to16Fallback", container.NeedsNode16Redirect.ToString() } + { "ContainerNode20to16Fallback", container.NeedsNode16Redirect.ToString() } } ); } } - } if (!string.IsNullOrEmpty(containerUserName)) @@ -1387,6 +1491,71 @@ private static void ThrowIfWrongWindowsVersion(IExecutionContext executionContex } } + /// + /// Creates appropriate handler data for container job based on knobs and custom node path. + /// Used by orchestrator to determine the best node version for the container. + /// + private BaseNodeHandlerData GetJobContainerHandlerData(IExecutionContext executionContext, ContainerInfo container) + { + // Custom node takes highest priority + if (!string.IsNullOrEmpty(container.CustomNodePath)) + { + return new CustomNodeHandlerData(); + } + + // Special platform requirement: macOS or Windows with Linux containers must use container's own node + if (PlatformUtil.RunningOnMacOS || (PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux)) + { + container.CustomNodePath = "node"; + return new CustomNodeHandlerData(); + } + + // Check knobs to determine default handler preference + bool useNode24 = AgentKnobs.UseNode24ToStartContainer.GetValue(executionContext).AsBoolean(); + bool useNode20 = AgentKnobs.UseNode20ToStartContainer.GetValue(executionContext).AsBoolean(); + + if (useNode24) + { + return new Node24HandlerData(); + } + else if (useNode20) + { + return new Node20_1HandlerData(); + } + else + { + return new Node20_1HandlerData(); + } + } + + /// + /// Uses the NodeVersionOrchestrator to determine the optimal node version for the container. + /// Sets container.ResultNodePath based on orchestrator decision. + /// + private void SetContainerNodePathWithOrchestrator(IExecutionContext executionContext, ContainerInfo container) + { + var handlerData = GetJobContainerHandlerData(executionContext, container); + + var taskContext = new NodeVersionStrategies.TaskContext + { + HandlerData = handlerData, + Container = container, + StepTarget = null + }; + + var orchestrator = new NodeVersionStrategies.NodeVersionOrchestrator(executionContext, HostContext); + var result = orchestrator.SelectNodeVersionForContainer(taskContext, _dockerManger); + + container.ResultNodePath = result.NodePath; + + if (!string.IsNullOrEmpty(result.Warning)) + { + executionContext.Warning(result.Warning); + } + + executionContext.Output($"Container node selection: {result.NodeVersion} - {result.Reason}"); + } + private void PublishTelemetry( IExecutionContext executionContext, object telemetryData, diff --git a/src/Agent.Worker/ExpressionManager.cs b/src/Agent.Worker/ExpressionManager.cs index 352a841643..578a0627b9 100644 --- a/src/Agent.Worker/ExpressionManager.cs +++ b/src/Agent.Worker/ExpressionManager.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.TeamFoundation.DistributedTask.Expressions; +using Microsoft.TeamFoundation.DistributedTask.Logging; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; using System; using System.Collections; using System.Collections.Generic; -using Microsoft.TeamFoundation.DistributedTask.WebApi; -using Microsoft.VisualStudio.Services.Agent.Util; -using Microsoft.TeamFoundation.DistributedTask.Expressions; -using Microsoft.TeamFoundation.DistributedTask.Logging; using System.Text; namespace Microsoft.VisualStudio.Services.Agent.Worker @@ -73,6 +73,8 @@ private sealed class TraceWriter : ITraceWriter public string Trace => _traceBuilder.ToString(); + bool ITraceWriter.VerboseTracing => false; + public TraceWriter(Tracing trace, IExecutionContext executionContext) { ArgUtil.NotNull(trace, nameof(trace)); diff --git a/src/Agent.Worker/Handlers/NodeHandler.cs b/src/Agent.Worker/Handlers/NodeHandler.cs index 222bf2eac3..0d9bc26153 100644 --- a/src/Agent.Worker/Handlers/NodeHandler.cs +++ b/src/Agent.Worker/Handlers/NodeHandler.cs @@ -4,6 +4,7 @@ using Agent.Sdk; using Agent.Sdk.Knob; using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies; using System.IO; using System.Text; using System.Threading.Tasks; @@ -14,6 +15,7 @@ using System.Collections.Generic; using System.Threading; using StringUtil = Microsoft.VisualStudio.Services.Agent.Util.StringUtil; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; namespace Microsoft.VisualStudio.Services.Agent.Worker.Handlers { @@ -30,6 +32,7 @@ public interface INodeHandlerHelper string[] GetFilteredPossibleNodeFolders(string nodeFolderName, string[] possibleNodeFolders); string GetNodeFolderPath(string nodeFolderName, IHostContext hostContext); bool IsNodeFolderExist(string nodeFolderName, IHostContext hostContext); + bool IsNodeExecutable(string nodeFolder, IHostContext HostContext, IExecutionContext ExecutionContext); } public class NodeHandlerHelper : INodeHandlerHelper @@ -50,11 +53,42 @@ public string[] GetFilteredPossibleNodeFolders(string nodeFolderName, string[] p possibleNodeFolders.Skip(nodeFolderIndex + 1).ToArray() : Array.Empty(); } + + public bool IsNodeExecutable(string nodeFolder, IHostContext HostContext, IExecutionContext ExecutionContext) + { + if (!this.IsNodeFolderExist(nodeFolder, HostContext)) + { + ExecutionContext.Debug($"Node folder does not exist: {nodeFolder}"); + return false; + } + var nodePath = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + const int NodeNotExecutableExitCode = 216; + try + { + var processInvoker = HostContext.CreateService(); + var exitCodeTask = processInvoker.ExecuteAsync( + workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Work), + fileName: nodePath, + arguments: "-v", + environment: null, + requireExitCodeZero: false, + outputEncoding: null, + cancellationToken: CancellationToken.None); + int exitCode = exitCodeTask.GetAwaiter().GetResult(); + return exitCode != NodeNotExecutableExitCode; + } + catch (Exception ex) + { + ExecutionContext.Debug($"Node executable test threw exception: {ex.Message}"); + return false; + } + } } public sealed class NodeHandler : Handler, INodeHandler { private readonly INodeHandlerHelper nodeHandlerHelper; + private readonly Lazy nodeVersionOrchestrator; private const string Node10Folder = "node10"; internal const string NodeFolder = "node"; internal static readonly string Node16Folder = "node16"; @@ -86,14 +120,22 @@ public sealed class NodeHandler : Handler, INodeHandler private bool? supportsNode20; private bool? supportsNode24; + // Fallback tracking for telemetry + private string fallbackReason; + private bool fallbackOccurred; + public NodeHandler() { this.nodeHandlerHelper = new NodeHandlerHelper(); + this.nodeVersionOrchestrator = new Lazy(() => + new NodeVersionOrchestrator(this.ExecutionContext, this.HostContext)); } public NodeHandler(INodeHandlerHelper nodeHandlerHelper) { this.nodeHandlerHelper = nodeHandlerHelper; + this.nodeVersionOrchestrator = new Lazy(() => + new NodeVersionOrchestrator(this.ExecutionContext, this.HostContext, nodeHandlerHelper)); } public BaseNodeHandlerData Data { get; set; } @@ -129,9 +171,10 @@ public async Task RunAsync() // Ensure compat vso-task-lib exist at the root of _work folder // This will make vsts-agent work against 2015 RTM/QU1 TFS, since tasks in those version doesn't package with task lib // Put the 0.5.5 version vso-task-lib into the root of _work/node_modules folder, so tasks are able to find those lib. - if (!File.Exists(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Work), "node_modules", "vso-task-lib", "package.json"))) + string vsoTaskLibFromExternal = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "vso-task-lib"); + if (Directory.Exists(vsoTaskLibFromExternal) && + !File.Exists(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Work), "node_modules", "vso-task-lib", "package.json"))) { - string vsoTaskLibFromExternal = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "vso-task-lib"); string compatVsoTaskLibInWork = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Work), "node_modules", "vso-task-lib"); IOUtil.CopyDirectory(vsoTaskLibFromExternal, compatVsoTaskLibInWork, ExecutionContext.CancellationToken); } @@ -162,6 +205,13 @@ public async Task RunAsync() } } + // Ensure working directory exists on disk before starting the task process. + if (!string.IsNullOrEmpty(workingDirectory) && !Directory.Exists(workingDirectory)) + { + Trace.Info($"Working directory does not exist, creating it: '{workingDirectory}'"); + Directory.CreateDirectory(workingDirectory); + } + // fix vsts-task-lib for node 6.x // vsts-task-lib 0.6/0.7/0.8/0.9/2.0-preview implemented String.prototype.startsWith and String.prototype.endsWith since Node 5.x doesn't have them. // however the implementation is added in node 6.x, the implementation in vsts-task-lib is different. @@ -184,60 +234,53 @@ public async Task RunAsync() StepHost.ErrorDataReceived += OnDataReceived; string file; - if (!string.IsNullOrEmpty(ExecutionContext.StepTarget()?.CustomNodePath)) - { - file = ExecutionContext.StepTarget().CustomNodePath; - } - else - { - bool useNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(ExecutionContext).AsBoolean(); - bool useNode24InUnsupportedSystem = AgentKnobs.UseNode24InUnsupportedSystem.GetValue(ExecutionContext).AsBoolean(); - bool node20ResultsInGlibCErrorHost = false; - bool node24ResultsInGlibCErrorHost = false; + bool useNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(ExecutionContext).AsBoolean(); + bool useNode24InUnsupportedSystem = AgentKnobs.UseNode24InUnsupportedSystem.GetValue(ExecutionContext).AsBoolean(); + bool node20ResultsInGlibCErrorHost = false; + bool node24ResultsInGlibCErrorHost = false; - if (PlatformUtil.HostOS == PlatformUtil.OS.Linux) + if (PlatformUtil.HostOS == PlatformUtil.OS.Linux) + { + if (!useNode20InUnsupportedSystem) { - if (!useNode20InUnsupportedSystem) + if (supportsNode20.HasValue) { - if (supportsNode20.HasValue) - { - node20ResultsInGlibCErrorHost = !supportsNode20.Value; - } - else - { - node20ResultsInGlibCErrorHost = await CheckIfNodeResultsInGlibCError(NodeHandler.Node20_1Folder); - ExecutionContext.EmitHostNode20FallbackTelemetry(node20ResultsInGlibCErrorHost); - supportsNode20 = !node20ResultsInGlibCErrorHost; - } + node20ResultsInGlibCErrorHost = !supportsNode20.Value; } - - if (!useNode24InUnsupportedSystem) + else { - if (supportsNode24.HasValue) - { - node24ResultsInGlibCErrorHost = !supportsNode24.Value; - } - else - { - node24ResultsInGlibCErrorHost = await CheckIfNodeResultsInGlibCError(NodeHandler.Node24Folder); - ExecutionContext.EmitHostNode24FallbackTelemetry(node24ResultsInGlibCErrorHost); - supportsNode24 = !node24ResultsInGlibCErrorHost; - } + node20ResultsInGlibCErrorHost = await CheckIfNodeResultsInGlibCError(NodeHandler.Node20_1Folder); + ExecutionContext.EmitHostNode20FallbackTelemetry(node20ResultsInGlibCErrorHost); + supportsNode20 = !node20ResultsInGlibCErrorHost; } } - ContainerInfo container = (ExecutionContext.StepTarget() as ContainerInfo); - if (container == null) + if (!useNode24InUnsupportedSystem) { - file = GetNodeLocation(node20ResultsInGlibCErrorHost, node24ResultsInGlibCErrorHost, inContainer: false); - } - else - { - file = GetNodeLocation(container.NeedsNode16Redirect, container.NeedsNode20Redirect, inContainer: true); + if (supportsNode24.HasValue) + { + node24ResultsInGlibCErrorHost = !supportsNode24.Value; + } + else + { + node24ResultsInGlibCErrorHost = await CheckIfNodeResultsInGlibCError(NodeHandler.Node24Folder); + ExecutionContext.EmitHostNode24FallbackTelemetry(node24ResultsInGlibCErrorHost); + supportsNode24 = !node24ResultsInGlibCErrorHost; + } } + } - ExecutionContext.Debug("Using node path: " + file); + ContainerInfo container = (ExecutionContext.StepTarget() as ContainerInfo); + if (container == null) + { + file = GetNodeLocation(node20ResultsInGlibCErrorHost, node24ResultsInGlibCErrorHost, inContainer: false); } + else + { + file = GetNodeLocation(container.NeedsNode16Redirect, container.NeedsNode20Redirect, inContainer: true); + } + + ExecutionContext.Debug("Using node path: " + file); // Format the arguments passed to node. // 1) Wrap the script file path in double quotes. @@ -320,6 +363,10 @@ public async Task RunAsync() private async Task CheckIfNodeResultsInGlibCError(string nodeFolder) { + if (!nodeHandlerHelper.IsNodeFolderExist(nodeFolder, HostContext)) + { + return true; + } var nodePath = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); List nodeVersionOutput = await ExecuteCommandAsync(ExecutionContext, nodePath, "-v", requireZeroExitCode: false, showOutputOnFailureOnly: true); var nodeResultsInGlibCError = WorkerUtilities.IsCommandResultGlibcError(ExecutionContext, nodeVersionOutput, out string nodeInfoLine); @@ -332,26 +379,37 @@ private string GetNodeFolderWithFallback(string preferredNodeFolder, bool node20 switch (preferredNodeFolder) { case var folder when folder == NodeHandler.Node24Folder: - if (node24ResultsInGlibCError) + // Fallback if Node24 has glibc error OR doesn't exist (e.g., win-x86) or not executable (e.g, windows 2012 R2) + bool node24NotExecutable = !nodeHandlerHelper.IsNodeExecutable(NodeHandler.Node24Folder, this.HostContext, this.ExecutionContext); + if (node24ResultsInGlibCError || node24NotExecutable) { - // Fallback to Node20, then Node16 if Node20 also fails - if (node20ResultsInGlibCError) + // Fallback to Node20, then Node16 if Node20 also fails or doesn't exist + bool node20NotAvailableForNode24Fallback = !nodeHandlerHelper.IsNodeFolderExist(NodeHandler.Node20_1Folder, HostContext); + if (node20ResultsInGlibCError || node20NotAvailableForNode24Fallback) { - NodeFallbackWarning("20", "16", inContainer); + fallbackReason = node24NotExecutable ? "NodeNotExecutable" : "GlibCError"; + fallbackOccurred = true; + NodeFallbackWarning("24", "16", inContainer, node24NotExecutable); return NodeHandler.Node16Folder; } else { - NodeFallbackWarning("24", "20", inContainer); + fallbackReason = node24NotExecutable ? "NodeNotExecutable" : "GlibCError"; + fallbackOccurred = true; + NodeFallbackWarning("24", "20", inContainer, node24NotExecutable); return NodeHandler.Node20_1Folder; } } return NodeHandler.Node24Folder; case var folder when folder == NodeHandler.Node20_1Folder: - if (node20ResultsInGlibCError) + // Fallback if Node20 has glibc error OR doesn't exist + bool node20NotAvailable = !nodeHandlerHelper.IsNodeFolderExist(NodeHandler.Node20_1Folder, HostContext); + if (node20ResultsInGlibCError || node20NotAvailable) { - NodeFallbackWarning("20", "16", inContainer); + fallbackReason = node20NotAvailable ? "NodeNotAvailable" : "GlibCError"; + fallbackOccurred = true; + NodeFallbackWarning("20", "16", inContainer, node20NotAvailable); return NodeHandler.Node16Folder; } return NodeHandler.Node20_1Folder; @@ -361,9 +419,48 @@ private string GetNodeFolderWithFallback(string preferredNodeFolder, bool node20 } } - public string GetNodeLocation(bool node20ResultsInGlibCError, bool node24ResultsInGlibCError, bool inContainer) { + bool useStrategyPattern = AgentKnobs.UseEnhancedNodeSelection.GetValue(ExecutionContext).AsBoolean(); + + if (useStrategyPattern) + { + ExecutionContext.Debug("Using enhanced node selection path for handler node resolution."); + return GetNodeLocationUsingStrategy(inContainer).GetAwaiter().GetResult(); + } + + ExecutionContext.Debug("Using legacy node selection path for handler node resolution."); + return GetNodeLocationLegacy(node20ResultsInGlibCError, node24ResultsInGlibCError, inContainer); + } + + private async Task GetNodeLocationUsingStrategy(bool inContainer) + { + try + { + var taskContext = new TaskContext + { + HandlerData = Data, + Container = inContainer ? (ExecutionContext.StepTarget() as ContainerInfo) : null, + StepTarget = inContainer ? null : ExecutionContext.StepTarget() + }; + + NodeRunnerInfo result = await nodeVersionOrchestrator.Value.SelectNodeVersionForHostAsync(taskContext); + return result.NodePath; + } + catch (Exception ex) + { + ExecutionContext.Error($"Strategy-based node selection failed: {ex.Message}"); + ExecutionContext.Debug($"Stack trace: {ex}"); + throw; + } + } + + private string GetNodeLocationLegacy(bool node20ResultsInGlibCError, bool node24ResultsInGlibCError, bool inContainer) + { + if (!string.IsNullOrEmpty(ExecutionContext.StepTarget()?.CustomNodePath)) + { + return ExecutionContext.StepTarget().CustomNodePath; + } bool useNode10 = AgentKnobs.UseNode10.GetValue(ExecutionContext).AsBoolean(); bool useNode20_1 = AgentKnobs.UseNode20_1.GetValue(ExecutionContext).AsBoolean(); bool UseNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(ExecutionContext).AsBoolean(); @@ -471,7 +568,7 @@ public string GetNodeLocation(bool node20ResultsInGlibCError, bool node24Results { try { - PublishHandlerTelemetry(nodeFolder); + PublishHandlerTelemetry(nodeFolder, inContainer); } catch (Exception ex) when (ex is FormatException || ex is ArgumentNullException || ex is NullReferenceException) { @@ -483,10 +580,14 @@ public string GetNodeLocation(bool node20ResultsInGlibCError, bool node24Results return nodeHandlerHelper.GetNodeFolderPath(nodeFolder, HostContext); } - private void NodeFallbackWarning(string fromVersion, string toVersion, bool inContainer) + private void NodeFallbackWarning(string fromVersion, string toVersion, bool inContainer, bool notExecutable = false) { string systemType = inContainer ? "container" : "agent"; - ExecutionContext.Warning($"The {systemType} operating system doesn't support Node{fromVersion}. Using Node{toVersion} instead. " + + string reason = notExecutable + ? $"Node{fromVersion} is not executable on this platform(e.g.,node binary missing or incompatible) " + : $"The {systemType} operating system doesn't support Node{fromVersion}"; + + ExecutionContext.Warning($"{reason}. Using Node{toVersion} instead. " + $"Please upgrade the operating system of the {systemType} to remain compatible with future updates of tasks: " + "https://github.com/nodesource/distributions"); } @@ -616,7 +717,7 @@ private async Task> ExecuteCommandAsync(IExecutionContext context, return outputs; } - private void PublishHandlerTelemetry(string realHandler) + private void PublishHandlerTelemetry(string realHandler, bool inContainer) { var systemVersion = PlatformUtil.GetSystemVersion(); string expectedHandler = ""; @@ -637,8 +738,12 @@ private void PublishHandlerTelemetry(string realHandler) { "OS", PlatformUtil.GetSystemId() ?? "" }, { "OSVersion", systemVersion?.Name?.ToString() ?? "" }, { "OSBuild", systemVersion?.Version?.ToString() ?? "" }, + { "Architecture", PlatformUtil.HostArchitecture.ToString() }, { "ExpectedExecutionHandler", expectedHandler }, { "RealExecutionHandler", realHandler }, + { "FallbackOccurred", fallbackOccurred.ToString() }, + { "FallbackReason", fallbackReason ?? "" }, + { "IsContainer", inContainer.ToString() }, { "JobId", ExecutionContext.Variables.System_JobId.ToString()}, { "PlanId", ExecutionContext.Variables.Get(Constants.Variables.System.PlanId)}, { "AgentName", ExecutionContext.Variables.Get(Constants.Variables.Agent.Name)}, @@ -650,4 +755,4 @@ private void PublishHandlerTelemetry(string realHandler) ExecutionContext.PublishTaskRunnerTelemetry(telemetryData); } } -} +} \ No newline at end of file diff --git a/src/Agent.Worker/JobExtension.cs b/src/Agent.Worker/JobExtension.cs index 05ac67fad9..261852a0e1 100644 --- a/src/Agent.Worker/JobExtension.cs +++ b/src/Agent.Worker/JobExtension.cs @@ -106,6 +106,27 @@ public async Task> InitializeJob(IExecutionContext jobContext, Pipel context.Warning(ex.Message); } } + if (!AgentKnobs.DisableUnsupportedOsWarningNet10.GetValue(context).AsBoolean()) + { + // Check if a system supports .NET 10 + try + { + Trace.Verbose("Checking if your system supports .NET 10"); + + // Check version of the system + if (!await PlatformUtil.IsNetVersionSupported("net10")) + { + string systemId = PlatformUtil.GetSystemId(); + SystemVersion systemVersion = PlatformUtil.GetSystemVersion(); + context.Warning(StringUtil.Loc("UnsupportedOsVersionByNet10", $"{systemId} {systemVersion}")); + } + } + catch (Exception ex) + { + Trace.Error($"Error has occurred while checking if system supports .NET 10: {ex}"); + context.Warning(ex.Message); + } + } // Set agent version variable. context.SetVariable(Constants.Variables.Agent.Version, BuildConstants.AgentPackage.Version); diff --git a/src/Agent.Worker/NodeVersionStrategies/CustomNodeStrategy.cs b/src/Agent.Worker/NodeVersionStrategies/CustomNodeStrategy.cs new file mode 100644 index 0000000000..a450cfbbb7 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/CustomNodeStrategy.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class CustomNodeStrategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + string customPath = null; + string source = null; + + if (context.Container == null && context.StepTarget != null) + { + customPath = context.StepTarget.CustomNodePath; + source = "StepTarget.CustomNodePath"; + } + else if (context.Container != null) + { + customPath = context.Container.CustomNodePath; + source = "Container.CustomNodePath"; + } + + if (string.IsNullOrWhiteSpace(customPath)) + { + executionContext.Debug("[CustomNodeStrategy] No custom node path found"); + return null; + } + + executionContext.Debug($"[CustomNodeStrategy] Found custom node path in {source}: {customPath}"); + + return new NodeRunnerInfo + { + NodePath = customPath, + NodeVersion = NodeVersion.Custom, + Reason = $"Custom Node.js path specified by user ({source})", + Warning = null + }; + } + + public NodeRunnerInfo CanHandleInContainer(TaskContext context, IExecutionContext executionContext, IDockerCommandManager dockerManager) + { + // Use the same logic as CanHandle, but specifically for container scenarios + return CanHandle(context, executionContext, null); + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfo.cs b/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfo.cs new file mode 100644 index 0000000000..ef1b7e2280 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfo.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Contains glibc compatibility information for different Node.js versions. + /// + public class GlibcCompatibilityInfo + { + /// + /// True if Node24 has glibc compatibility errors (requires glibc 2.28+). + /// + public bool Node24HasGlibcError { get; set; } + + /// + /// True if Node20 has glibc compatibility errors (requires glibc 2.17+). + /// + public bool Node20HasGlibcError { get; set; } + + /// + /// Creates a new instance with no glibc errors (compatible system). + /// + public static GlibcCompatibilityInfo Compatible => new GlibcCompatibilityInfo + { + Node24HasGlibcError = false, + Node20HasGlibcError = false + }; + + /// + /// Creates a new instance with specific compatibility information. + /// + public static GlibcCompatibilityInfo Create(bool node24HasGlibcError, bool node20HasGlibcError) => + new GlibcCompatibilityInfo + { + Node24HasGlibcError = node24HasGlibcError, + Node20HasGlibcError = node20HasGlibcError + }; + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfoProvider.cs b/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfoProvider.cs new file mode 100644 index 0000000000..d55c5276dc --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfoProvider.cs @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using Agent.Sdk; +using Agent.Sdk.Knob; +using Microsoft.VisualStudio.Services.Agent.Util; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Utility class for checking glibc compatibility with Node.js versions on Linux systems. + /// + public class GlibcCompatibilityInfoProvider : AgentService, IGlibcCompatibilityInfoProvider + { + private static bool? _supportsNode20; + private static bool? _supportsNode24; + + public GlibcCompatibilityInfoProvider() + { + // Parameterless constructor for dependency injection + } + + public GlibcCompatibilityInfoProvider(IHostContext hostContext) + { + ArgUtil.NotNull(hostContext, nameof(hostContext)); + base.Initialize(hostContext); + } + + /// + /// Checks glibc compatibility for both Node20 and Node24. + /// This method combines the behavior from NodeHandler for both Node versions. + /// + /// GlibcCompatibilityInfo containing compatibility results for both Node versions + public virtual async Task CheckGlibcCompatibilityAsync(IExecutionContext _executionContext) + { + bool useNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(_executionContext).AsBoolean(); + bool useNode24InUnsupportedSystem = AgentKnobs.UseNode24InUnsupportedSystem.GetValue(_executionContext).AsBoolean(); + + bool node20HasGlibcError = false; + bool node24HasGlibcError = false; + + // Only perform glibc compatibility checks on Linux systems + if (!IsLinuxPlatform()) + { + // Non-Linux systems (Windows, macOS) don't have glibc compatibility issues + return GlibcCompatibilityInfo.Create(node24HasGlibcError: false, node20HasGlibcError: false); + } + + if (!useNode20InUnsupportedSystem) + { + if (_supportsNode20.HasValue) + { + node20HasGlibcError = !_supportsNode20.Value; + } + else + { + node20HasGlibcError = await CheckIfNodeResultsInGlibCErrorAsync("node20_1", _executionContext); + _executionContext.EmitHostNode20FallbackTelemetry(node20HasGlibcError); + _supportsNode20 = !node20HasGlibcError; + } + } + + if (!useNode24InUnsupportedSystem) + { + if (_supportsNode24.HasValue) + { + node24HasGlibcError = !_supportsNode24.Value; + } + else + { + node24HasGlibcError = await CheckIfNodeResultsInGlibCErrorAsync("node24", _executionContext); + _executionContext.EmitHostNode24FallbackTelemetry(node24HasGlibcError); + _supportsNode24 = !node24HasGlibcError; + } + } + + return GlibcCompatibilityInfo.Create(node24HasGlibcError, node20HasGlibcError); + } + + /// + /// Gets glibc compatibility information based on the execution context (host vs container). + /// + /// The task context containing container and handler information + /// Glibc compatibility information for the current execution environment + public virtual async Task GetGlibcCompatibilityAsync(TaskContext context, IExecutionContext _executionContext) + { + ArgUtil.NotNull(context, nameof(context)); + + string environmentType = context.Container != null ? "Container" : "Host"; + + if (context.Container == null) + { + // Host execution - check actual glibc compatibility + var glibcInfo = await CheckGlibcCompatibilityAsync(_executionContext); + + _executionContext.Debug($"[{environmentType}] Host glibc compatibility - Node24: {!glibcInfo.Node24HasGlibcError}, Node20: {!glibcInfo.Node20HasGlibcError}"); + + return glibcInfo; + } + else + { + // Container execution - use container-specific redirect information + var glibcInfo = GlibcCompatibilityInfo.Create( + node24HasGlibcError: context.Container.NeedsNode20Redirect, + node20HasGlibcError: context.Container.NeedsNode16Redirect); + + _executionContext.Debug($"[{environmentType}] Container glibc compatibility - Node24: {!glibcInfo.Node24HasGlibcError}, Node20: {!glibcInfo.Node20HasGlibcError}"); + + return glibcInfo; + } + } + + /// + /// Checks if the specified Node.js version results in glibc compatibility errors. + /// + /// The node folder name (e.g., "node20_1", "node24") + /// True if glibc error is detected, false otherwise + public virtual async Task CheckIfNodeResultsInGlibCErrorAsync(string nodeFolder, IExecutionContext _executionContext) + { + var nodePath = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + if (!NodeBinaryExists(nodePath)) + { + return true; + } + List nodeVersionOutput = await ExecuteCommandAsync(_executionContext, nodePath, "-v", requireZeroExitCode: false, showOutputOnFailureOnly: true); + var nodeResultsInGlibCError = WorkerUtilities.IsCommandResultGlibcError(_executionContext, nodeVersionOutput, out string nodeInfoLine); + + return nodeResultsInGlibCError; + } + + protected virtual bool NodeBinaryExists(string nodePath) + { + return File.Exists(nodePath); + } + + /// + /// Determines if the current platform is Linux. Virtual for testing override. + /// + /// True if running on Linux, false otherwise + protected virtual bool IsLinuxPlatform() + { + return PlatformUtil.HostOS == PlatformUtil.OS.Linux; + } + + private async Task> ExecuteCommandAsync(IExecutionContext context, string command, string arg, bool requireZeroExitCode, bool showOutputOnFailureOnly) + { + string commandLog = $"{command} {arg}"; + if (!showOutputOnFailureOnly) + { + context.Command(commandLog); + } + + List outputs = new List(); + object outputLock = new object(); + var processInvoker = HostContext.CreateService(); + processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) + { + if (!string.IsNullOrEmpty(message.Data)) + { + lock (outputLock) + { + outputs.Add(message.Data); + } + } + }; + + processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) + { + if (!string.IsNullOrEmpty(message.Data)) + { + lock (outputLock) + { + outputs.Add(message.Data); + } + } + }; + + var exitCode = await processInvoker.ExecuteAsync( + workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Work), + fileName: command, + arguments: arg, + environment: null, + requireExitCodeZero: requireZeroExitCode, + outputEncoding: null, + cancellationToken: System.Threading.CancellationToken.None); + + if (!showOutputOnFailureOnly || exitCode != 0) + { + if (showOutputOnFailureOnly) + { + context.Command(commandLog); + } + + foreach (var outputLine in outputs) + { + context.Debug(outputLine); + } + } + + return outputs; + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/IGlibcCompatibilityInfoProvider.cs b/src/Agent.Worker/NodeVersionStrategies/IGlibcCompatibilityInfoProvider.cs new file mode 100644 index 0000000000..8088060fd9 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/IGlibcCompatibilityInfoProvider.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.VisualStudio.Services.Agent; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Interface for checking glibc compatibility with Node.js versions on Linux systems. + /// + [ServiceLocator(Default = typeof(GlibcCompatibilityInfoProvider))] + public interface IGlibcCompatibilityInfoProvider : IAgentService + { + /// + /// Checks glibc compatibility for both Node20 and Node24. + /// + /// GlibcCompatibilityInfo containing compatibility results for both Node versions + Task CheckGlibcCompatibilityAsync(IExecutionContext executionContext); + + /// + /// Gets glibc compatibility information, adapting to execution context (host vs container). + /// + /// Task execution context for determining environment + /// The execution context for logging and knob access + /// GlibcCompatibilityInfo containing compatibility results for both Node versions + Task GetGlibcCompatibilityAsync(TaskContext context, IExecutionContext executionContext); + + /// + /// Checks if the specified Node.js version results in glibc compatibility errors. + /// + /// The node folder name (e.g., "node20_1", "node24") + /// The execution context for logging and telemetry + /// True if glibc error is detected, false otherwise + Task CheckIfNodeResultsInGlibCErrorAsync(string nodeFolder, IExecutionContext executionContext); + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/INodeVersionStrategy.cs b/src/Agent.Worker/NodeVersionStrategies/INodeVersionStrategy.cs new file mode 100644 index 0000000000..589a2e83d7 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/INodeVersionStrategy.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Strategy interface for both host and container node selection. + /// + public interface INodeVersionStrategy + { + + /// + /// Evaluates if this strategy can handle the given context and determines the node version to use. + /// Includes handler type checks, knob evaluation, EOL policy enforcement, and glibc compatibility. + /// + /// Context with environment, task, and glibc information + /// Execution context for knob evaluation + /// Glibc compatibility information for Node versions + /// NodeRunnerInfo with selected version and metadata if this strategy can handle the context, null if it cannot handle + /// Thrown when EOL policy prevents using any compatible version + NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo); + + /// + /// Evaluates if this strategy can handle container execution and determines the node version to use. + /// Only Node24, Node20, and Node16 strategies support container execution. + /// + /// Context with container and task information + /// Execution context for knob evaluation + /// Docker command manager for container operations + /// NodeRunnerInfo with selected version and metadata if this strategy can handle container execution, null if it cannot handle or doesn't support containers + /// Thrown when EOL policy prevents using any compatible version + NodeRunnerInfo CanHandleInContainer(TaskContext context, IExecutionContext executionContext, IDockerCommandManager dockerManager) + { + // Default implementation: older strategies (Node10, Node6) don't support container execution + return null; + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/Node10Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node10Strategy.cs new file mode 100644 index 0000000000..fd786c89de --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node10Strategy.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node10Strategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + string taskName = executionContext.Variables.Get(Constants.Variables.Task.DisplayName) ?? "Unknown Task"; + + if (context.EffectiveMaxVersion < 10) + { + executionContext.Debug($"[Node10Strategy] EffectiveMaxVersion={context.EffectiveMaxVersion} < 10, skipping"); + return null; + } + + if (eolPolicyEnabled) + { + throw new NotSupportedException(StringUtil.Loc("NodeEOLPolicyBlocked", "Node10")); + } + + if (context.HandlerData is Node10HandlerData) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node10, + Reason = "Selected for Node10 task handler", + Warning = StringUtil.Loc("NodeEOLRetirementWarning", taskName) + }; + } + + bool isAlpine = PlatformUtil.RunningOnAlpine; + if (isAlpine) + { + executionContext.Warning( + "Using Node10 on Alpine Linux because Node6 is not compatible. " + + "Node10 has reached End-of-Life. Please upgrade to Node20 or Node24 for continued support."); + + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node10, + Reason = "Selected for Alpine Linux compatibility (Node6 incompatible)", + Warning = StringUtil.Loc("NodeEOLRetirementWarning", taskName) + }; + } + + return null; + } + + } +} diff --git a/src/Agent.Worker/NodeVersionStrategies/Node16Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node16Strategy.cs new file mode 100644 index 0000000000..edd79e773c --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node16Strategy.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node16Strategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + string taskName = executionContext.Variables.Get(Constants.Variables.Task.DisplayName) ?? "Unknown Task"; + + if (context.EffectiveMaxVersion < 16) + { + executionContext.Debug($"[Node16Strategy] EffectiveMaxVersion={context.EffectiveMaxVersion} < 16, skipping"); + return null; + } + + if (eolPolicyEnabled) + { + executionContext.Debug(StringUtil.Loc("NodeEOLFallbackBlocked", "Node16")); + throw new NotSupportedException(StringUtil.Loc("NodeEOLPolicyBlocked", "Node16")); + } + + if (context.HandlerData is Node16HandlerData) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node16, + Reason = "Selected for Node16 task handler", + Warning = StringUtil.Loc("NodeEOLRetirementWarning", taskName) + }; + } + + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node16, + Reason = "Fallback to Node16 due to glibc issues", + Warning = StringUtil.Loc("NodeEOLRetirementWarning", taskName) + }; + } + + public NodeRunnerInfo CanHandleInContainer(TaskContext context, IExecutionContext executionContext, IDockerCommandManager dockerManager) + { + if (context.Container == null) + { + executionContext.Debug("[Node16Strategy] CanHandleInContainer called but no container context provided"); + return null; + } + + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + + if (eolPolicyEnabled) + { + executionContext.Debug("[Node16Strategy] Node16 blocked by EOL policy in container"); + throw new NotSupportedException("No compatible Node.js version available for container execution. Node16 is blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks."); + } + + executionContext.Debug("[Node16Strategy] Providing Node16 as final fallback for container"); + + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node16, + Reason = "Final fallback to Node16 for container execution", + Warning = "Using Node16 in container. Consider updating to Node20 or Node24 for better performance and security." + }; + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/Node20Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node20Strategy.cs new file mode 100644 index 0000000000..b821eedb0f --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node20Strategy.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node20Strategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool useNode20Globally = AgentKnobs.UseNode20_1.GetValue(executionContext).AsBoolean(); + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + + string taskName = executionContext.Variables.Get(Constants.Variables.Task.DisplayName) ?? "Unknown Task"; + + if (glibcInfo.Node20HasGlibcError) + { + executionContext.Debug("[Node20Strategy] Node20 has glibc compatibility issue, skipping"); + return null; + } + + if (useNode20Globally) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node20, + Reason = "Selected via global AGENT_USE_NODE20_1 override", + Warning = null + }; + } + + if (eolPolicyEnabled) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node20, + Reason = "Upgraded from end-of-life Node version due to EOL policy", + Warning = context.EffectiveMaxVersion <= NodeVersionHelper.MaxEOLNodeVersion ? StringUtil.Loc("NodeEOLUpgradeWarning", taskName) : null + }; + } + + if (context.EffectiveMaxVersion < 20) + { + executionContext.Debug($"[Node20Strategy] EffectiveMaxVersion={context.EffectiveMaxVersion} < 20, skipping"); + return null; + } + + if (context.HandlerData is Node20_1HandlerData) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node20, + Reason = "Selected for Node20 task handler", + Warning = null + }; + } + + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node20, + Reason = "Fallback to Node20", + Warning = StringUtil.Loc("NodeGlibcFallbackWarning", "agent", "Node24", "Node20") + }; + } + + public NodeRunnerInfo CanHandleInContainer(TaskContext context, IExecutionContext executionContext, IDockerCommandManager dockerManager) + { + if (context.Container == null) + { + executionContext.Debug("[Node20Strategy] CanHandleInContainer called but no container context provided"); + return null; + } + + bool useNode20ToStartContainer = AgentKnobs.UseNode20ToStartContainer.GetValue(executionContext).AsBoolean(); + if (!useNode20ToStartContainer) + { + executionContext.Debug("[Node20Strategy] UseNode20ToStartContainer=false, cannot handle container"); + return null; + } + + executionContext.Debug("[Node20Strategy] UseNode20ToStartContainer=true, checking Node20 availability in container"); + + try + { + if (NodeContainerTestHelper.CanExecuteNodeInContainer(context, executionContext, dockerManager, NodeVersion.Node20, "Node20Strategy")) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node20, + Reason = "Node20 available in container via UseNode20ToStartContainer knob", + Warning = null + }; + } + else + { + executionContext.Debug("[Node20Strategy] Node20 test failed in container, returning null for fallback"); + return null; + } + } + catch (Exception ex) + { + executionContext.Warning($"[Node20Strategy] Failed to test Node20 in container: {ex.Message}"); + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/Node24Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node24Strategy.cs new file mode 100644 index 0000000000..23d6140eab --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node24Strategy.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; +using Microsoft.VisualStudio.Services.Agent.Worker.Handlers; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node24Strategy : INodeVersionStrategy + { + private readonly INodeHandlerHelper _nodeHandlerHelper; + + public Node24Strategy(INodeHandlerHelper nodeHandlerHelper) + { + _nodeHandlerHelper = nodeHandlerHelper ?? throw new ArgumentNullException(nameof(nodeHandlerHelper)); + } + + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool useNode24Globally = AgentKnobs.UseNode24.GetValue(executionContext).AsBoolean(); + bool useNode24WithHandlerData = AgentKnobs.UseNode24withHandlerData.GetValue(executionContext).AsBoolean(); + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + var hostContext = executionContext.GetHostContext(); + string node24Folder = NodeVersionHelper.GetFolderName(NodeVersion.Node24); + string taskName = executionContext.Variables.Get(Constants.Variables.Task.DisplayName) ?? "Unknown Task"; + + if (!_nodeHandlerHelper.IsNodeExecutable(node24Folder, hostContext, executionContext)) + { + executionContext.Debug("[Node24Strategy] Node24 not executable on this platform (e.g., node binary missing or incompatible or exit code 216), checking fallback options"); + return null; + } + + if (glibcInfo.Node24HasGlibcError) + { + executionContext.Debug(StringUtil.Loc("NodeGlibcFallbackWarning", "agent", "Node24", "Node20")); + return null; + } + + if (useNode24Globally) + { + executionContext.Debug("[Node24Strategy] AGENT_USE_NODE24=true, global override"); + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node24, + Reason = "Selected via global AGENT_USE_NODE24 override", + Warning = null + }; + } + + if (eolPolicyEnabled) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node24, + Reason = "Upgraded from end-of-life Node version due to EOL policy", + Warning = context.EffectiveMaxVersion <= NodeVersionHelper.MaxEOLNodeVersion ? StringUtil.Loc("NodeEOLUpgradeWarning", taskName) : null + }; + } + + if (context.EffectiveMaxVersion < 24) + { + executionContext.Debug($"[Node24Strategy] EffectiveMaxVersion={context.EffectiveMaxVersion} < 24, skipping"); + return null; + } + + if (context.HandlerData is Node24HandlerData) + { + if (!useNode24WithHandlerData) + { + executionContext.Debug("[Node24Strategy] Node24 handler detected but UseNode24withHandlerData=false, skipping"); + return null; + } + + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node24, + Reason = "Selected for Node24 task handler", + Warning = null + }; + } + + return null; + } + + public NodeRunnerInfo CanHandleInContainer(TaskContext context, IExecutionContext executionContext, IDockerCommandManager dockerManager) + { + if (context.Container == null) + { + executionContext.Debug("[Node24Strategy] CanHandleInContainer called but no container context provided"); + return null; + } + + bool useNode24ToStartContainer = AgentKnobs.UseNode24ToStartContainer.GetValue(executionContext).AsBoolean(); + + if (!useNode24ToStartContainer) + { + executionContext.Debug("[Node24Strategy] UseNode24ToStartContainer=false, cannot handle container"); + return null; + } + + executionContext.Debug("[Node24Strategy] UseNode24ToStartContainer=true, checking Node24 availability in container"); + + try + { + if (NodeContainerTestHelper.CanExecuteNodeInContainer(context, executionContext, dockerManager, NodeVersion.Node24, "Node24Strategy")) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node24, + Reason = "Node24 available in container via UseNode24ToStartContainer knob", + Warning = null + }; + } + else + { + executionContext.Debug("[Node24Strategy] Node24 test failed in container, returning null for fallback"); + return null; + } + } + catch (Exception ex) + { + executionContext.Warning($"[Node24Strategy] Failed to test Node24 in container: {ex.Message}"); + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/Node6Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node6Strategy.cs new file mode 100644 index 0000000000..0c09ce4ae9 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node6Strategy.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node6Strategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + bool hasNode6Handler = context.HandlerData != null && context.HandlerData.GetType() == typeof(NodeHandlerData); + + string taskName = executionContext.Variables.Get(Constants.Variables.Task.DisplayName) ?? "Unknown Task"; + + if (eolPolicyEnabled) + { + throw new NotSupportedException(StringUtil.Loc("NodeEOLPolicyBlocked", "Node6")); + } + + if (hasNode6Handler) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = NodeVersion.Node6, + Reason = "Selected for Node6 task handler", + Warning = StringUtil.Loc("NodeEOLRetirementWarning", taskName) + }; + } + + return null; + } + } +} diff --git a/src/Agent.Worker/NodeVersionStrategies/NodeContainerTestHelper.cs b/src/Agent.Worker/NodeVersionStrategies/NodeContainerTestHelper.cs new file mode 100644 index 0000000000..43be9ab3c7 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/NodeContainerTestHelper.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using Agent.Sdk; +using Agent.Sdk.Knob; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Helper class for testing Node.js availability in containers. + /// + public static class NodeContainerTestHelper + { + /// + /// Tests if a specific Node version can execute in the container. + /// Cross-platform scenarios are handled earlier in the orchestrator. + /// + public static bool CanExecuteNodeInContainer(TaskContext context, IExecutionContext executionContext, IDockerCommandManager dockerManager, NodeVersion nodeVersion, string strategyName) + { + var container = context.Container; + ArgUtil.NotNull(container, nameof(container)); + ArgUtil.NotNull(container.ContainerId, nameof(container.ContainerId)); + + try + { + executionContext.Debug($"[{strategyName}] Testing {nodeVersion} availability in container {container.ContainerId}"); + + var hostContext = executionContext.GetHostContext(); + string nodeFolder = NodeVersionHelper.GetFolderName(nodeVersion); + string hostPath = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + string containerNodePath = container.TranslateToContainerPath(hostPath); + + // Fix path and extension for target container OS + if (container.ImageOS == PlatformUtil.OS.Linux) + { + containerNodePath = containerNodePath.Replace('\\', '/'); + if (containerNodePath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + { + containerNodePath = containerNodePath.Substring(0, containerNodePath.Length - 4); + } + } + executionContext.Debug($"[{strategyName}] Testing path: {containerNodePath}"); + + bool result = ExecuteNodeTestCommand(context, executionContext, dockerManager, containerNodePath, strategyName, $"agent {nodeVersion} binaries"); + return result; + } + catch (Exception ex) + { + executionContext.Debug($"[{strategyName}] Exception testing {nodeVersion}: {ex.Message}"); + return false; + } + } + + /// + /// Executes the node --version command in the container to test Node.js availability. + /// + private static bool ExecuteNodeTestCommand(TaskContext context, IExecutionContext executionContext, IDockerCommandManager dockerManager, string nodePath, string strategyName, string nodeDescription) + { + var container = context.Container; + + try + { + var output = new List(); + + // Format command following the same pattern as ContainerOperationProvider startup commands + string testCommand; + if (PlatformUtil.RunningOnWindows) + { + if (container.ImageOS == PlatformUtil.OS.Windows) + { + testCommand = $"cmd.exe /c \"\"{nodePath}\" --version\""; + } + else + { + testCommand = $"bash -c \"{nodePath} --version\""; + } + } + else + { + testCommand = $"bash -c \"{nodePath} --version\""; + } + + executionContext.Debug($"[{strategyName}] Testing {nodeDescription} with command: {testCommand}"); + int exitCode = dockerManager.DockerExec(executionContext, container.ContainerId, string.Empty, testCommand, output).Result; + + if (exitCode == 0 && output.Count > 0) + { + executionContext.Debug($"[{strategyName}] {nodeDescription} test successful: {output[0]}"); + return true; + } + else + { + executionContext.Debug($"[{strategyName}] {nodeDescription} test failed with exit code {exitCode}"); + return false; + } + } + catch (Exception ex) + { + executionContext.Debug($"[{strategyName}] Exception testing {nodeDescription}: {ex.Message}"); + return false; + } + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/NodeRunnerInfo.cs b/src/Agent.Worker/NodeVersionStrategies/NodeRunnerInfo.cs new file mode 100644 index 0000000000..5ecccd5bdf --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/NodeRunnerInfo.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Represents the available Node.js versions supported by the agent. + /// + public enum NodeVersion + { + Node6, + Node10, + Node16, + Node20, + Node24, + Custom, + ContainerDefaultNode + } + + /// + /// Helper class for NodeVersion operations. + /// + public static class NodeVersionHelper + { + /// + /// The highest Node version considered end-of-life. + /// Tasks with EffectiveMaxVersion at or below this threshold get an EOL upgrade warning. + /// + public const int MaxEOLNodeVersion = 16; + + /// + /// Gets the folder name for the specified NodeVersion. + /// + public static string GetFolderName(NodeVersion version) + { + return version switch + { + NodeVersion.Node6 => "node", + NodeVersion.Node10 => "node10", + NodeVersion.Node16 => "node16", + NodeVersion.Node20 => "node20_1", + NodeVersion.Node24 => "node24", + NodeVersion.Custom => "custom", + NodeVersion.ContainerDefaultNode => "container_default_node", + _ => throw new ArgumentOutOfRangeException(nameof(version)) + }; + } + } + + /// + /// Result containing the selected Node path and metadata. + /// Used by strategy pattern for both host and container node selection. + /// + public sealed class NodeRunnerInfo + { + /// + /// Full path to the node executable. + /// + public string NodePath { get; set; } + + /// + /// The node version selected. + /// + public NodeVersion NodeVersion { get; set; } + + /// + /// Explanation of why this version was selected. + /// Used for debugging and telemetry. + /// + public string Reason { get; set; } + + /// + /// Optional warning message to display to user. + /// Example: "Container OS doesn't support Node24, using Node20 instead." + /// + public string Warning { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/NodeVersionOrchestrator.cs b/src/Agent.Worker/NodeVersionStrategies/NodeVersionOrchestrator.cs new file mode 100644 index 0000000000..4a754300c9 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/NodeVersionOrchestrator.cs @@ -0,0 +1,260 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Agent.Sdk; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; +using Microsoft.VisualStudio.Services.Agent.Worker.Handlers; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class NodeVersionOrchestrator + { + private readonly List _strategies; + private readonly IExecutionContext ExecutionContext; + private readonly IHostContext HostContext; + private readonly IGlibcCompatibilityInfoProvider GlibcChecker; + + public NodeVersionOrchestrator(IExecutionContext executionContext, IHostContext hostContext) + : this(executionContext, hostContext, new NodeHandlerHelper()) + { + } + + public NodeVersionOrchestrator(IExecutionContext executionContext, IHostContext hostContext, INodeHandlerHelper nodeHandlerHelper) + { + ArgUtil.NotNull(executionContext, nameof(executionContext)); + ArgUtil.NotNull(hostContext, nameof(hostContext)); + ArgUtil.NotNull(nodeHandlerHelper, nameof(nodeHandlerHelper)); + ExecutionContext = executionContext; + HostContext = hostContext; + GlibcChecker = HostContext.GetService(); + GlibcChecker.Initialize(hostContext); + _strategies = new List(); + + // IMPORTANT: Strategy order determines selection priority + // Add strategies in descending priority order (newest/preferred versions first) + // The orchestrator will try each strategy in order until one can handle the request + _strategies.Add(new CustomNodeStrategy()); + _strategies.Add(new Node24Strategy(nodeHandlerHelper)); + _strategies.Add(new Node20Strategy()); + _strategies.Add(new Node16Strategy()); + _strategies.Add(new Node10Strategy()); + _strategies.Add(new Node6Strategy()); + } + + /// + /// Host-specific node version selection using CanHandle methods. + /// Follows the standard strategy precedence: Custom Node → Node24 → Node20 → Node16 → Node10 → Node6. + /// + public async Task SelectNodeVersionForHostAsync(TaskContext context) + { + string environmentType = "Host"; + ExecutionContext.Debug($"[{environmentType}] Starting node version selection"); + ExecutionContext.Debug($"[{environmentType}] Handler type: {context.HandlerData?.GetType().Name ?? "null"}"); + + var glibcInfo = await GlibcChecker.GetGlibcCompatibilityAsync(context, ExecutionContext); + + foreach (var strategy in _strategies) + { + ExecutionContext.Debug($"[{environmentType}] Checking strategy: {strategy.GetType().Name}"); + + try + { + var selectionResult = strategy.CanHandle(context, ExecutionContext, glibcInfo); + if (selectionResult != null) + { + var result = CreateNodeRunnerInfoWithPath(context, selectionResult); + + // Publish telemetry for monitoring node version selection via Kusto + PublishNodeVersionSelectionTelemetry(result, strategy, environmentType, context); + + ExecutionContext.Output( + $"[{environmentType}] Selected Node version: {result.NodeVersion} (Strategy: {strategy.GetType().Name})"); + ExecutionContext.Debug($"[{environmentType}] Node path: {result.NodePath}"); + ExecutionContext.Debug($"[{environmentType}] Reason: {result.Reason}"); + + if (!string.IsNullOrEmpty(result.Warning)) + { + ExecutionContext.Warning(result.Warning); + } + + return result; + } + else + { + ExecutionContext.Debug($"[{environmentType}] Strategy '{strategy.GetType().Name}' cannot handle this context"); + } + } + catch (NotSupportedException ex) + { + ExecutionContext.Debug($"[{environmentType}] Strategy '{strategy.GetType().Name}' blocked: {ex.Message} - trying next strategy"); + } + catch (Exception ex) + { + ExecutionContext.Warning($"[{environmentType}] Strategy '{strategy.GetType().Name}' threw unexpected exception: {ex.Message} - trying next strategy"); + } + } + + string handlerType = context.HandlerData?.GetType().Name ?? "Unknown"; + ExecutionContext.Debug(StringUtil.Loc("NodeVersionNotAvailable", handlerType)); + throw new NotSupportedException($"No compatible Node.js version available for host execution. Handler type: {handlerType}. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false"); + } + + /// + /// Gets strategies that support container execution (Custom node, Node24, Node20, Node16). + /// Only these strategies have CanHandleInContainer implementations. + /// + private IEnumerable GetContainerCapableStrategies() + { + return _strategies.Take(4); + } + + /// + /// Container-specific node version selection using CanHandleInContainer methods. + /// Follows the container knob precedence: Custom Node → Node24 → Node20 → Node16. + /// + public NodeRunnerInfo SelectNodeVersionForContainer(TaskContext context, IDockerCommandManager dockerManager) + { + string environmentType = "Container"; + ExecutionContext.Debug($"[{environmentType}] Starting container node version selection"); + ExecutionContext.Debug($"[{environmentType}] Handler type: {context.HandlerData?.GetType().Name ?? "null"}"); + + if (PlatformUtil.RunningOnMacOS || (PlatformUtil.RunningOnWindows && context.Container.ImageOS == PlatformUtil.OS.Linux)) + { + ExecutionContext.Debug($"[{environmentType}] Cross-platform scenario detected - using container's built-in Node.js"); + ExecutionContext.Debug($"[{environmentType}] Agent OS: {(PlatformUtil.RunningOnMacOS ? "macOS" : "Windows")}, Container OS: {context.Container.ImageOS}"); + + var crossPlatformResult = new NodeRunnerInfo + { + NodePath = "node", + NodeVersion = NodeVersion.ContainerDefaultNode, + Reason = "Cross-platform scenario requires container's built-in Node.js" + }; + + ExecutionContext.Output($"[{environmentType}] Selected Node version: {crossPlatformResult.NodeVersion} (Cross-platform fallback)"); + ExecutionContext.Debug($"[{environmentType}] Node path: {crossPlatformResult.NodePath}"); + ExecutionContext.Debug($"[{environmentType}] Reason: {crossPlatformResult.Reason}"); + + return crossPlatformResult; + } + + foreach (var strategy in GetContainerCapableStrategies()) + { + ExecutionContext.Debug($"[{environmentType}] Checking container strategy: {strategy.GetType().Name}"); + + try + { + var selectionResult = strategy.CanHandleInContainer(context, ExecutionContext, dockerManager); + if (selectionResult != null) + { + var result = CreateNodeRunnerInfoWithPath(context, selectionResult); + + PublishNodeVersionSelectionTelemetry(result, strategy, environmentType, context); + + ExecutionContext.Output( + $"[{environmentType}] Selected Node version: {result.NodeVersion} (Strategy: {strategy.GetType().Name})"); + ExecutionContext.Debug($"[{environmentType}] Node path: {result.NodePath}"); + ExecutionContext.Debug($"[{environmentType}] Reason: {result.Reason}"); + + if (!string.IsNullOrEmpty(result.Warning)) + { + ExecutionContext.Warning(result.Warning); + } + + return result; + } + else + { + ExecutionContext.Debug($"[{environmentType}] Strategy '{strategy.GetType().Name}' cannot handle this container context"); + } + } + catch (NotSupportedException ex) + { + ExecutionContext.Debug($"[{environmentType}] Strategy '{strategy.GetType().Name}' threw NotSupportedException: {ex.Message}"); + ExecutionContext.Error($"Container node version selection failed: {ex.Message}"); + throw; + } + catch (Exception ex) + { + ExecutionContext.Warning($"[{environmentType}] Strategy '{strategy.GetType().Name}' threw unexpected exception: {ex.Message} - trying next strategy"); + } + } + + throw new NotSupportedException("No Node.js version could be selected for container execution. Please check your container knobs and node availability."); + } + + private NodeRunnerInfo CreateNodeRunnerInfoWithPath(TaskContext context, NodeRunnerInfo selection) + { + if (selection.NodeVersion == NodeVersion.Custom) + { + return selection; + } + string externalsPath = HostContext.GetDirectory(WellKnownDirectory.Externals); + string nodeFolder = NodeVersionHelper.GetFolderName(selection.NodeVersion); + + if (context.Container != null) + { + // Container execution: use agent binaries (cross-platform scenarios are handled earlier in SelectNodeVersionForContainer) + string containerExeExtension = context.Container.ImageOS == PlatformUtil.OS.Windows ? ".exe" : ""; + string hostPath = Path.Combine(externalsPath, nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + string containerNodePath = context.Container.TranslateToContainerPath(hostPath); + // Fix the executable extension for the container OS + string finalPath = containerNodePath.Replace($"node{IOUtil.ExeExtension}", $"node{containerExeExtension}"); + + return new NodeRunnerInfo + { + NodePath = finalPath, + NodeVersion = selection.NodeVersion, + Reason = selection.Reason, + Warning = selection.Warning + }; + } + else + { + // Host execution: use host OS executable extension + string hostPath = Path.Combine(externalsPath, nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + + return new NodeRunnerInfo + { + NodePath = hostPath, + NodeVersion = selection.NodeVersion, + Reason = selection.Reason, + Warning = selection.Warning + }; + } + } + + private void PublishNodeVersionSelectionTelemetry(NodeRunnerInfo result, INodeVersionStrategy strategy, string environmentType, TaskContext context) + { + try + { + var telemetryData = new Dictionary + { + { "NodeVersion", result.NodeVersion.ToString() }, + { "Strategy", strategy.GetType().Name }, + { "EnvironmentType", environmentType }, + { "HandlerType", context.HandlerData?.GetType().Name ?? "Unknown" }, + { "SelectionReason", result.Reason ?? "" }, + { "HasWarning", (!string.IsNullOrEmpty(result.Warning)).ToString() }, + { "JobId", ExecutionContext.Variables.System_JobId.ToString() }, + { "PlanId", ExecutionContext.Variables.Get(Constants.Variables.System.PlanId) ?? "" }, + { "AgentName", ExecutionContext.Variables.Get(Constants.Variables.Agent.Name) ?? "" }, + { "IsContainer", (context.Container != null).ToString() }, + { "Architecture", PlatformUtil.HostArchitecture.ToString() } + }; + + ExecutionContext.PublishTaskRunnerTelemetry(telemetryData); + } + catch (Exception ex) + { + ExecutionContext.Debug($"Failed to publish node version selection telemetry: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/TaskContext.cs b/src/Agent.Worker/NodeVersionStrategies/TaskContext.cs new file mode 100644 index 0000000000..33a49fb376 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/TaskContext.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Agent.Sdk; +using Microsoft.TeamFoundation.DistributedTask.WebApi; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Context for both host and container node selection. + /// Contains runtime data - strategies read their own knobs via ExecutionContext. + /// + public sealed class TaskContext + { + /// + /// The handler data from the task definition. + /// + public BaseNodeHandlerData HandlerData { get; set; } + + /// + /// Container information for path translation. Null for host execution. + /// + public ContainerInfo Container { get; set; } + + /// + /// Step target for custom node path lookup. Null for container execution. + /// + public ExecutionTargetInfo StepTarget { get; set; } + + /// + /// Returns the maximum Node version this task was authored to run on, + /// derived from the handler data type. + /// Strategies use this as a ceiling: if EffectiveMaxVersion is less than their version, they return null. + /// Global overrides and EOL policy bypass this ceiling. + /// + public int EffectiveMaxVersion + { + get + { + return HandlerData switch + { + Node24HandlerData => 24, + Node20_1HandlerData => 20, + Node16HandlerData => 16, + Node10HandlerData => 10, + NodeHandlerData => 6, + _ => 6 + }; + } + } + } +} diff --git a/src/Agent.Worker/Release/Artifacts/CustomArtifact.cs b/src/Agent.Worker/Release/Artifacts/CustomArtifact.cs index 7ab64b368a..3ab48c0396 100644 --- a/src/Agent.Worker/Release/Artifacts/CustomArtifact.cs +++ b/src/Agent.Worker/Release/Artifacts/CustomArtifact.cs @@ -2,19 +2,19 @@ // Licensed under the MIT License. using Agent.Sdk; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Services.ServiceEndpoints.Common; using Microsoft.TeamFoundation.DistributedTask.WebApi; using Microsoft.VisualStudio.Services.Agent.Util; using Microsoft.VisualStudio.Services.Agent.Worker.Release.Artifacts.Definition; using Microsoft.VisualStudio.Services.ReleaseManagement.WebApi.Contracts; -using ServiceEndpointContracts = Microsoft.VisualStudio.Services.ServiceEndpoints.WebApi; +using Microsoft.VisualStudio.Services.ServiceEndpoints.Common; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using ServiceEndpointContracts = Microsoft.VisualStudio.Services.ServiceEndpoints.WebApi; namespace Microsoft.VisualStudio.Services.Agent.Worker.Release.Artifacts { @@ -29,6 +29,7 @@ public async Task DownloadAsync(IExecutionContext executionContext, ArtifactDefi ArgUtil.NotNull(artifactDefinition, nameof(artifactDefinition)); EnsureVersionBelongsToLinkedDefinition(artifactDefinition); + IRsaProvider rsaProvider = new AsnRsaProvider(); var customArtifactDetails = artifactDefinition.Details as CustomArtifactDetails; if (customArtifactDetails != null) @@ -41,7 +42,7 @@ public async Task DownloadAsync(IExecutionContext executionContext, ArtifactDefi customArtifactDetails.ResultTemplate, customArtifactDetails.AuthorizationHeaders?.Select(header => ToAuthorizationHeader(header)).ToList(), customArtifactDetails.ArtifactVariables, - new BouncyCastleRsaProvider()); + rsaProvider); var artifactDownloadDetailList = new List(); artifactDetails.ToList().ForEach(x => artifactDownloadDetailList.Add(JToken.Parse(x).ToObject())); @@ -125,7 +126,7 @@ private async Task DownloadArtifact( IEndpointAuthorizer authorizer = SchemeBasedAuthorizerFactory.GetEndpointAuthorizer( ToServiceEndpoint(customArtifactDetails.Endpoint), customArtifactDetails.AuthorizationHeaders?.Select(header => ToAuthorizationHeader(header)).ToList(), - new BouncyCastleRsaProvider()); + new AsnRsaProvider()); using (HttpWebResponse webResponse = GetWebResponse(executionContext, artifact.DownloadUrl, authorizer)) { @@ -171,9 +172,9 @@ private static string GetArtifactStreamType(CustomArtifactDownloadDetails artifa private static HttpWebResponse GetWebResponse(IExecutionContext executionContext, string url, IEndpointAuthorizer authorizer) { - #pragma warning disable SYSLIB0014 +#pragma warning disable SYSLIB0014 var request = WebRequest.Create(url) as HttpWebRequest; - #pragma warning restore SYSLIB0014 +#pragma warning restore SYSLIB0014 if (request == null) { string errorMessage = StringUtil.Loc("RMArtifactDownloadRequestCreationFailed", url); @@ -181,7 +182,13 @@ private static HttpWebResponse GetWebResponse(IExecutionContext executionContext throw new InvalidOperationException(errorMessage); } - authorizer.AuthorizeRequest(request, null); + if (!authorizer.TryAuthorizeRequest(request, null)) + { + string authError = StringUtil.Loc("RMErrorDuringArtifactDownload", $"Authorization failed for URL: {url}"); + executionContext.Output(authError); + throw new InvalidOperationException(authError); + } + var webResponse = request.GetResponseAsync().Result as HttpWebResponse; return webResponse; } @@ -201,7 +208,7 @@ private void EnsureVersionBelongsToLinkedDefinition(ArtifactDefinition artifactD customArtifactDetails.VersionsResultTemplate, customArtifactDetails.AuthorizationHeaders?.Select(header => ToAuthorizationHeader(header)).ToList(), customArtifactDetails.ArtifactVariables, - new BouncyCastleRsaProvider()); + new AsnRsaProvider()); foreach (var version in versions) { diff --git a/src/Agent.Worker/TaskManager.cs b/src/Agent.Worker/TaskManager.cs index b5c33e7134..65847fecbd 100644 --- a/src/Agent.Worker/TaskManager.cs +++ b/src/Agent.Worker/TaskManager.cs @@ -906,6 +906,10 @@ public sealed class Node24HandlerData : BaseNodeHandlerData { public override int Priority => 101; } + public sealed class CustomNodeHandlerData : BaseNodeHandlerData + { + public override int Priority => 100; + } public sealed class PowerShell3HandlerData : HandlerData { diff --git a/src/Agent.Worker/TestResults/ResultsCommandExtension.cs b/src/Agent.Worker/TestResults/ResultsCommandExtension.cs index f6e0d47b15..e54d85beb8 100644 --- a/src/Agent.Worker/TestResults/ResultsCommandExtension.cs +++ b/src/Agent.Worker/TestResults/ResultsCommandExtension.cs @@ -51,10 +51,9 @@ public sealed class PublishTestResultsCommand : IWorkerCommand private string _testPlanId; private bool _publishTestResultsLibFeatureState; private bool _triggerCoverageMergeJobFeatureState; - private bool _failTaskOnFailedTests; - private string _testRunSystem; + private bool _isDetectTestRunRetry; //telemetry parameter private const string _telemetryFeature = "PublishTestResultsCommand"; @@ -172,6 +171,13 @@ private void LoadPublishTestResultsInputs(IExecutionContext context, Dictionary< { _testPlanId = testPlanId; } + string isDetectTestRunRetry; + eventProperties.TryGetValue(PublishTestResultsEventProperties.IsDetectTestRunRetry, out isDetectTestRunRetry); + if (string.IsNullOrEmpty(isDetectTestRunRetry) || !bool.TryParse(isDetectTestRunRetry, out _isDetectTestRunRetry)) + { + // if no proper input is provided by default we do not detect test run retry. + _isDetectTestRunRetry = false; + } } private void LogPublishTestResultsFailureWarning(Exception ex) @@ -297,7 +303,8 @@ private PublishOptions GetPublishOptions() var publishOptions = new PublishOptions() { IsMergeTestResultsToSingleRun = _mergeResults, - IsAddTestRunAttachments = _publishRunLevelAttachments + IsAddTestRunAttachments = _publishRunLevelAttachments, + IsDetectTestRunRetry = _isDetectTestRunRetry }; return publishOptions; @@ -457,5 +464,6 @@ internal static class PublishTestResultsEventProperties public static readonly string FailTaskOnFailedTests = "failTaskOnFailedTests"; public static readonly string ListOfAutomatedTestPoints = "listOfAutomatedTestPoints"; public static readonly string TestPlanId = "testPlanId"; + public static readonly string IsDetectTestRunRetry = "isDetectTestRunRetry"; } } diff --git a/src/Agent.Worker/TestResults/TestDataPublisher.cs b/src/Agent.Worker/TestResults/TestDataPublisher.cs index 2a355ce5f6..e04262bf69 100644 --- a/src/Agent.Worker/TestResults/TestDataPublisher.cs +++ b/src/Agent.Worker/TestResults/TestDataPublisher.cs @@ -76,7 +76,6 @@ public void InitializePublisher(IExecutionContext context, string projectName, V if (testDataProvider != null) { var testRunData = testDataProvider.GetTestRunData(); - //publishing run level attachment Task> publishtestRunDataTask = Task.Run(() => _testRunPublisher.PublishTestRunDataAsync(runContext, _projectName, testRunData, publishOptions, cancellationToken)); Task uploadBuildDataAttachmentTask = Task.Run(() => UploadBuildDataAttachment(runContext, testDataProvider.GetBuildData(), cancellationToken)); @@ -89,7 +88,18 @@ public void InitializePublisher(IExecutionContext context, string projectName, V IList publishedRuns = publishtestRunDataTask.Result; - var isTestRunOutcomeFailed = GetTestRunOutcome(_executionContext, testRunData, out TestRunSummary testRunSummary); + bool isTestRunOutcomeFailed; + TestRunSummary testRunSummary; + if (publishOptions.IsDetectTestRunRetry) + { + DetectAndSetRetriesForTestRun(testRunData); + isTestRunOutcomeFailed = GetTestRunOutcomeForRetries(testRunData, out testRunSummary); + } + else + { + // For non-retry-aware publishing, determine test run outcome based on the primary run results (legacy behavior) + isTestRunOutcomeFailed = GetTestRunOutcome(_executionContext, testRunData, out testRunSummary); + } // Storing testrun summary in environment variable, which will be read by PublishPipelineMetadataTask and publish to evidence store. if (_calculateTestRunSummary) @@ -197,7 +207,18 @@ public void InitializePublisher(IExecutionContext context, string projectName, V IList publishedRuns = publishtestRunDataTask.Result; - var isTestRunOutcomeFailed = GetTestRunOutcome(_executionContext, testRunData, out TestRunSummary testRunSummary); + bool isTestRunOutcomeFailed; + TestRunSummary testRunSummary; + if (publishOptions.IsDetectTestRunRetry) + { + DetectAndSetRetriesForTestRun(testRunData); + isTestRunOutcomeFailed = GetTestRunOutcomeForRetries(testRunData, out testRunSummary); + } + else + { + // For non-retry-aware publishing, determine test run outcome based on the primary run results (legacy behavior) + isTestRunOutcomeFailed = GetTestRunOutcome(_executionContext, testRunData, out testRunSummary); + } // Storing testrun summary in environment variable, which will be read by PublishPipelineMetadataTask and publish to evidence store. if (_calculateTestRunSummary) @@ -332,5 +353,159 @@ private async Task GetProjectId(string projectName) return proj.Id; } + + #region Test retry helper + /// + /// Detects retry test runs by grouping them based on TestRunIdFromAttachmentFile. + /// Modifies the input list in place: retry runs are removed from the top-level list + /// and added to the primary run's collection. + /// + /// Mutable list of parsed test run data. + internal void DetectAndSetRetriesForTestRun(IList testRunDataList) + { + if (testRunDataList == null || testRunDataList.Count <= 1) + { + return; + } + + // Group by TestRunIdFromAttachmentFile – runs that share the same ID are retries + var groupedByRunId = testRunDataList + .Where(t => !string.IsNullOrEmpty(t.TestRunIdFromAttachmentFile)) + .GroupBy(t => t.TestRunIdFromAttachmentFile) + .Where(g => g.Count() > 1) + .ToList(); + + if (groupedByRunId.Count == 0) + { + return; + } + + var retryRunsToRemove = new HashSet(); + + foreach (var group in groupedByRunId) + { + // Sort by TestRunStartDate – the earliest is the primary run, the rest are retries + var sortedRuns = group + .OrderBy(t => t.TestRunStartDate) + .ToList(); + + var primaryRun = sortedRuns[0]; + primaryRun.Retries = new List(); + + for (int i = 1; i < sortedRuns.Count; i++) + { + primaryRun.Retries.Add(sortedRuns[i]); + retryRunsToRemove.Add(sortedRuns[i]); + } + } + + // Remove retry runs from the main list so only primary runs remain + foreach (var retryRun in retryRunsToRemove) + { + testRunDataList.Remove(retryRun); + } + } + + /// + /// Gets the latest attempt result for each test in a run that has retries. + /// Returns a dictionary mapping test identifier to its latest outcome. + /// Later retry attempts override earlier outcomes for the same test. + /// + internal Dictionary GetLatestAttemptResults(TestRunData testRunData) + { + var latestResults = new Dictionary(); + + // Process primary run results first + ProcessTestResults(testRunData, latestResults); + + // Process each retry in order – later retries override earlier ones + if (testRunData.Retries != null) + { + foreach (var retry in testRunData.Retries) + { + ProcessTestResults(retry, latestResults); + } + } + + return latestResults; + } + + /// + /// Checks whether any test is still marked as failed after all retry attempts + /// and computes a based on the final outcome per test. + /// For runs with retries, only the latest attempt outcome per test is considered. + /// For runs without retries, the standard outcome check is applied. + /// + /// The list of test run data (primary runs only; retries are nested). + /// + /// When this method returns, contains a whose counters + /// reflect only the latest attempt per test (i.e., intermediate retry results are not double-counted). + /// + /// + /// true if at least one test is still failing after all retry attempts; + /// false if all previously-failed tests were resolved by retries or there are no failures. + /// + internal bool GetTestRunOutcomeForRetries(IList testRunDataList, out TestRunSummary testRunSummary) + { + _executionContext?.Debug("isDetectTestRunRetry: Detecting test run retries for outcome evaluation."); + testRunSummary = new TestRunSummary(); + + if (testRunDataList == null || testRunDataList.Count == 0) + { + return false; + } + + bool anyFailedTests = false; + + foreach (var testRunData in testRunDataList) + { + // GetLatestAttemptResults works for both cases: + // - With retries: returns only the final outcome per test across all attempts + // - Without retries: returns each test's outcome from the primary run + var latestAttemptResults = GetLatestAttemptResults(testRunData); + foreach (var outcome in latestAttemptResults.Values) + { + testRunSummary.Total += 1; + switch (outcome) + { + case TestOutcome.Failed: + case TestOutcome.Aborted: + testRunSummary.Failed += 1; + anyFailedTests = true; + break; + case TestOutcome.Passed: + testRunSummary.Passed += 1; + break; + case TestOutcome.Inconclusive: + testRunSummary.Skipped += 1; + break; + default: + break; + } + } + } + return anyFailedTests; + } + + + internal static void ProcessTestResults(TestRunData testRunData, Dictionary latestResults) + { + if (testRunData?.TestResults == null) + { + return; + } + + foreach (var result in testRunData.TestResults) + { + string testName = String.Concat(result.AutomatedTestName, " ", result.TestCaseTitle); + if (!string.IsNullOrEmpty(testName) && Enum.TryParse(result.Outcome, true, out TestOutcome parsedOutcome)) + { + latestResults[testName] = parsedOutcome; + } + } + } + + #endregion + } } diff --git a/src/Agent.Worker/VsoTaskLibManager.cs b/src/Agent.Worker/VsoTaskLibManager.cs index 8cb41b5e9a..857eb5fb58 100644 --- a/src/Agent.Worker/VsoTaskLibManager.cs +++ b/src/Agent.Worker/VsoTaskLibManager.cs @@ -41,8 +41,19 @@ public static async Task DownloadVsoTaskLibAsync(IExecutionContext executionCont public static async Task DownloadAsync(IExecutionContext executionContext, string blobUrl, string tempDirectory, string extractPath, IRetryOptions retryOptions) { - Directory.CreateDirectory(tempDirectory); - Directory.CreateDirectory(extractPath); + if (!IOUtil.TryCreateDirectory(tempDirectory)) + { + executionContext.Warning($"Cannot create directory '{tempDirectory}': permission denied. " + + "This may occur in container environments. Skipping vso-task-lib download."); + return; + } + if (!IOUtil.TryCreateDirectory(extractPath)) + { + executionContext.Warning($"Cannot create directory '{extractPath}': permission denied. " + + "This may occur in container environments. Skipping vso-task-lib download."); + IOUtil.DeleteDirectory(tempDirectory, CancellationToken.None); + return; + } string downloadPath = Path.ChangeExtension(Path.Combine(tempDirectory, "download"), ".tar.gz"); string toolName = new DirectoryInfo(extractPath).Name; @@ -99,7 +110,11 @@ public static async Task DownloadAsync(IExecutionContext executionContext, strin /// private static void ExtractTarGz(string tarGzPath, string extractPath, IExecutionContext executionContext, string toolName) { - Directory.CreateDirectory(extractPath); + if (!IOUtil.TryCreateDirectory(extractPath)) + { + executionContext.Warning($"Cannot create directory '{extractPath}' for extraction: permission denied."); + return; + } executionContext.Debug($"Extracting {toolName} using tar..."); using (var process = new System.Diagnostics.Process { diff --git a/src/Common.props b/src/Common.props index 19c298e182..8b39801b81 100644 --- a/src/Common.props +++ b/src/Common.props @@ -1,7 +1,8 @@ 10.0 - net8.0;net6.0 + $(NetTargetFramework) + net8.0 $(PackageRuntime) true true @@ -11,7 +12,7 @@ OS_UNKNOWN ARCH_UNKNOWN - 0.5.251-private + 0.5.270-private $(CodeAnalysis) false false @@ -78,4 +79,12 @@ $(OSPlatform);$(OSArchitecture);$(DebugConstant);TRACE + + + + + all + runtime; build; native; contentfiles; analyzers + + diff --git a/src/Microsoft.VisualStudio.Services.Agent/AgentServer.cs b/src/Microsoft.VisualStudio.Services.Agent/AgentServer.cs index 336afd358f..14b5adb478 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/AgentServer.cs +++ b/src/Microsoft.VisualStudio.Services.Agent/AgentServer.cs @@ -11,6 +11,7 @@ using Microsoft.VisualStudio.Services.WebApi; using Microsoft.VisualStudio.Services.Common; using Agent.Sdk.Util; +using Agent.Sdk.Knob; namespace Microsoft.VisualStudio.Services.Agent { @@ -26,9 +27,9 @@ public interface IAgentServer : IAgentService { Task ConnectAsync(Uri serverUrl, VssCredentials credentials); - Task RefreshConnectionAsync(AgentConnectionType connectionType, TimeSpan timeout); + Task RefreshConnectionAsync(AgentConnectionType connectionType, TimeSpan? timeout = null); - void SetConnectionTimeout(AgentConnectionType connectionType, TimeSpan timeout); + void ResetConnectionTimeout(AgentConnectionType connectionType, TimeSpan? timeout = null); // Configuration Task AddAgentAsync(Int32 agentPoolId, TaskAgent agent); @@ -73,12 +74,22 @@ public async Task ConnectAsync(Uri serverUrl, VssCredentials credentials) // Establish the first connection before doing the rest in parallel to eliminate the redundant 401s. // issue: https://github.com/microsoft/azure-pipelines-agent/issues/3149 - Task task1 = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(100)); + + // Read timeout from environment variable (VSTS_HTTP_TIMEOUT), default to 100 seconds + TimeSpan connectionTimeout = GetConnectionTimeout(AgentConnectionType.Generic, null); + Task task1 = EstablishVssConnection(serverUrl, credentials, connectionTimeout); _genericConnection = await task1; - Task task2 = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(60)); - Task task3 = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(60)); + // MessageQueue connection for long-polling - uses full timeout from environment variable + TimeSpan messageQueueTimeout = GetConnectionTimeout(AgentConnectionType.MessageQueue, null); + Task task2 = EstablishVssConnection(serverUrl, credentials, messageQueueTimeout); + + // JobRequest connection uses capped timeout (max 100s) to ensure job lock renewals + // happen within the ~5 minute lock window. With 60s between renewals, timeout must be + // < 240s to avoid lock expiration, so we cap at 100s for safety. + TimeSpan jobRequestTimeout = GetConnectionTimeout(AgentConnectionType.JobRequest, null); + Task task3 = EstablishVssConnection(serverUrl, credentials, jobRequestTimeout); await Task.WhenAll(task2, task3); @@ -95,8 +106,10 @@ public async Task ConnectAsync(Uri serverUrl, VssCredentials credentials) } // Refresh connection is best effort. it should never throw exception - public async Task RefreshConnectionAsync(AgentConnectionType connectionType, TimeSpan timeout) + public async Task RefreshConnectionAsync(AgentConnectionType connectionType, TimeSpan? timeout = null) { + TimeSpan actualTimeout = GetConnectionTimeout(connectionType, timeout); + Trace.Info($"Refresh {connectionType} VssConnection to get on a different AFD node."); VssConnection newConnection = null; switch (connectionType) @@ -105,7 +118,7 @@ public async Task RefreshConnectionAsync(AgentConnectionType connectionType, Tim try { _hasMessageConnection = false; - newConnection = await EstablishVssConnection(_messageConnection.Uri, _messageConnection.Credentials, timeout); + newConnection = await EstablishVssConnection(_messageConnection.Uri, _messageConnection.Credentials, actualTimeout); var client = newConnection.GetClient(); _messageConnection = newConnection; _messageTaskAgentClient = client; @@ -130,7 +143,7 @@ public async Task RefreshConnectionAsync(AgentConnectionType connectionType, Tim try { _hasRequestConnection = false; - newConnection = await EstablishVssConnection(_requestConnection.Uri, _requestConnection.Credentials, timeout); + newConnection = await EstablishVssConnection(_requestConnection.Uri, _requestConnection.Credentials, actualTimeout); var client = newConnection.GetClient(); _requestConnection = newConnection; _requestTaskAgentClient = client; @@ -155,7 +168,7 @@ public async Task RefreshConnectionAsync(AgentConnectionType connectionType, Tim try { _hasGenericConnection = false; - newConnection = await EstablishVssConnection(_genericConnection.Uri, _genericConnection.Credentials, timeout); + newConnection = await EstablishVssConnection(_genericConnection.Uri, _genericConnection.Credentials, actualTimeout); var client = newConnection.GetClient(); _genericConnection = newConnection; _genericTaskAgentClient = client; @@ -182,19 +195,21 @@ public async Task RefreshConnectionAsync(AgentConnectionType connectionType, Tim } } - public void SetConnectionTimeout(AgentConnectionType connectionType, TimeSpan timeout) + public void ResetConnectionTimeout(AgentConnectionType connectionType, TimeSpan? timeout = null) { - Trace.Info($"Set {connectionType} VssConnection's timeout to {timeout.TotalSeconds} seconds."); + TimeSpan actualTimeout = GetConnectionTimeout(connectionType, timeout); + + Trace.Info($"Set {connectionType} VssConnection's timeout to {actualTimeout.TotalSeconds} seconds."); switch (connectionType) { case AgentConnectionType.JobRequest: - _requestConnection.Settings.SendTimeout = timeout; + _requestConnection.Settings.SendTimeout = actualTimeout; break; case AgentConnectionType.MessageQueue: - _messageConnection.Settings.SendTimeout = timeout; + _messageConnection.Settings.SendTimeout = actualTimeout; break; case AgentConnectionType.Generic: - _genericConnection.Settings.SendTimeout = timeout; + _genericConnection.Settings.SendTimeout = actualTimeout; break; default: Trace.Error($"Unexpected connection type: {connectionType}."); @@ -229,6 +244,40 @@ private async Task EstablishVssConnection(Uri serverUrl, VssCrede throw new InvalidOperationException(nameof(EstablishVssConnection)); } + /// + /// Gets the connection timeout for the specified connection type. + /// Reads from VSTS_HTTP_TIMEOUT environment variable with valid range [100, 1200] seconds. + /// JobRequest connections are capped at 60s to ensure job lock renewals complete within + /// the ~5 minute lock window (60s timeout + 60s delay = 120s < 300s). + /// + /// Type of connection + /// Optional explicit timeout (overrides environment variable but not type-specific caps) + /// Timeout value to use + private TimeSpan GetConnectionTimeout(AgentConnectionType connectionType, TimeSpan? timeout) + { + TimeSpan actualTimeout; + + if (timeout.HasValue) + { + actualTimeout = timeout.Value; + } + else + { + // Read from environment variable, clamped to valid range [100, 1200] + int httpRequestTimeoutSeconds = AgentKnobs.HttpTimeout.GetValue(HostContext).AsInt(); + actualTimeout = TimeSpan.FromSeconds(Math.Min(Math.Max(httpRequestTimeoutSeconds, 100), 1200)); + } + + // Cap JobRequest timeout to 60s to ensure renewals complete within job lock window + // This applies to both explicit and environment variable timeouts + if (connectionType == AgentConnectionType.JobRequest) + { + return TimeSpan.FromSeconds(Math.Min(actualTimeout.TotalSeconds, 60)); + } + + return actualTimeout; + } + private void CheckConnection(AgentConnectionType connectionType) { switch (connectionType) diff --git a/src/Microsoft.VisualStudio.Services.Agent/Constants.cs b/src/Microsoft.VisualStudio.Services.Agent/Constants.cs index ce764ed8a5..8fe8d9f48f 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/Constants.cs +++ b/src/Microsoft.VisualStudio.Services.Agent/Constants.cs @@ -74,6 +74,10 @@ public static class MicrosoftExtensionTaskIds public static readonly Guid TemplateAnalyzerSarif = new Guid("2ff4011a-8c38-46ae-9654-29d7d45ce875"); public static readonly Guid TerrascanSarif = new Guid("f1af679c-4cbf-4952-98c9-c772c8eb9920"); public static readonly Guid TrivySarif = new Guid("93e29b44-e118-445d-b809-ae3c7907bee7"); + + // enterpriselivemigration-tasks + public static readonly Guid ElmPrecheckTask = new Guid("95f6d7c1-6cc0-47c2-944c-6172489de134"); + public static readonly Guid ElmRepoSyncTask = new Guid("3a0763a8-1b93-4553-a749-947a21c45b3f"); } public static List RequiredForTelemetry = new() @@ -93,7 +97,9 @@ public static class MicrosoftExtensionTaskIds MicrosoftExtensionTaskIds.AdvancedSecurityDependencyScanning, MicrosoftExtensionTaskIds.TemplateAnalyzerSarif, MicrosoftExtensionTaskIds.TerrascanSarif, - MicrosoftExtensionTaskIds.TrivySarif + MicrosoftExtensionTaskIds.TrivySarif, + MicrosoftExtensionTaskIds.ElmPrecheckTask, + MicrosoftExtensionTaskIds.ElmRepoSyncTask }; } diff --git a/src/Microsoft.VisualStudio.Services.Agent/JobNotification.cs b/src/Microsoft.VisualStudio.Services.Agent/JobNotification.cs index 7d310b2fa3..bac2505034 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/JobNotification.cs +++ b/src/Microsoft.VisualStudio.Services.Agent/JobNotification.cs @@ -18,7 +18,7 @@ public interface IJobNotification : IAgentService, IDisposable { Task JobStarted(Guid jobId, string accessToken, Uri serverUrl, Guid planId, string identifier, string definitionId, string planType); Task JobCompleted(Guid jobId); - void StartClient(string pipeName, string monitorSocketAddress, CancellationToken cancellationToken); + Task StartClient(string pipeName, string monitorSocketAddress, CancellationToken cancellationToken); void StartClient(string socketAddress, string monitorSocketAddress); } @@ -101,13 +101,15 @@ public async Task JobCompleted(Guid jobId) } } - public async void StartClient(string pipeName, string monitorSocketAddress, CancellationToken cancellationToken) + public async Task StartClient(string pipeName, string monitorSocketAddress, CancellationToken cancellationToken) { if (pipeName != null && !_configured) { Trace.Info("Connecting to named pipe {0}", pipeName); _outClient = new NamedPipeClientStream(".", pipeName, PipeDirection.Out, PipeOptions.Asynchronous); - await _outClient.ConnectAsync(cancellationToken); + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(30)); + await _outClient.ConnectAsync(timeoutCts.Token); _writeStream = new StreamWriter(_outClient, Encoding.UTF8); _configured = true; Trace.Info("Connection successful to named pipe {0}", pipeName); diff --git a/src/Microsoft.VisualStudio.Services.Agent/Microsoft.VisualStudio.Services.Agent.csproj b/src/Microsoft.VisualStudio.Services.Agent/Microsoft.VisualStudio.Services.Agent.csproj index a48413526f..896f6638eb 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/Microsoft.VisualStudio.Services.Agent.csproj +++ b/src/Microsoft.VisualStudio.Services.Agent/Microsoft.VisualStudio.Services.Agent.csproj @@ -12,17 +12,17 @@ - + - - + + - + - - + + diff --git a/src/Misc/externals.sh b/src/Misc/externals.sh index 09f1f73d02..cfeea89261 100644 --- a/src/Misc/externals.sh +++ b/src/Misc/externals.sh @@ -31,9 +31,9 @@ NODE_VERSION="6.17.1" NODE10_VERSION="10.24.1" NODE16_VERSION="16.20.2" NODE16_WIN_ARM64_VERSION="16.9.1" -NODE20_VERSION="20.19.4" -NODE24_VERSION="24.10.0" -MINGIT_VERSION="2.50.1" +NODE20_VERSION="20.20.2" +NODE24_VERSION="24.15.0" +MINGIT_VERSION="2.53.0" LFS_VERSION="3.4.0" get_abs_path() { @@ -126,6 +126,15 @@ function acquireExternalTool() { echo "Testing tar gz" tar xzf "$download_target" -C "$extract_dir" > /dev/null || checkRC 'tar' fi + + if [[ "$download_basename" == node-v*.tar.gz ]]; then + echo "Cleaning Node.js distribution extract - removing unused npm/lib" + find "$extract_dir" -path "*/lib/node_modules" -type d -exec rm -rf {} + 2>/dev/null || true + find "$extract_dir" \( -name "npm" -o -name "npx" -o -name "corepack" \) -not -type d -delete 2>/dev/null || true + find "$extract_dir" -path "*/include" -type d -exec rm -rf {} + 2>/dev/null || true + find "$extract_dir" -path "*/share" -type d -exec rm -rf {} + 2>/dev/null || true + find "$extract_dir" \( -name "CHANGELOG.md" -o -name "README.md" \) -delete 2>/dev/null || true + fi fi else # Extract to layout. diff --git a/src/Misc/layoutbin/de-DE/strings.json b/src/Misc/layoutbin/de-DE/strings.json index 62e2b3dc1c..dd0c643702 100644 --- a/src/Misc/layoutbin/de-DE/strings.json +++ b/src/Misc/layoutbin/de-DE/strings.json @@ -10,6 +10,7 @@ "AddEnvironmentVMResourceTags": "Umgebungstags für VM-Ressourcen? (J/N)", "AgentAddedSuccessfully": "Der Agent wurde erfolgreich hinzugefügt.", "AgentAlreadyInsideContainer": "Das Containerfeature wird nicht unterstützt, wenn Agent darin bereits ausgeführt wird. Schlagen Sie in der Dokumentation (https://go.microsoft.com/fwlink/?linkid=875268) nach.", + "AgentCdnAccessFailWarning": "Aktion erforderlich: Der Azure Pipelines-Agent kann die neue CDN-URL nicht erreichen. Setzen Sie jetzt „download.agent.dev.azure.com“ auf die Positivliste, um Pipelinefehler zu vermeiden. Details: https://devblogs.microsoft.com/devops/cdn-domain-url-change-for-agents-in-pipelines/", "AgentDoesNotSupportContainerFeatureRhel6": "Der Agent bietet unter Red Hat Enterprise Linux 6 oder CentOS 6 keine Unterstützung für das Containerfeature.", "AgentDowngrade": "Der Agent wird auf eine niedrigere Version herabgestuft. Die ist in der Regel auf einen Rollback des aktuell veröffentlichten Agents zur Fehlerkorrektur zurückzuführen. Um dieses Verhalten zu deaktivieren, legen Sie die Umgebungsvariable AZP_AGENT_DOWNGRADE_DISABLED auf TRUE fest, bevor Sie Ihren Agent starten.", "AgentExit": "Der Agent wird kurzzeitig für die Aktualisierung beendet. Er sollte innerhalb von 10 Sekunden wieder online sein.", @@ -107,6 +108,7 @@ " Aushandeln (Kerberos oder NTLM)", " ALT (Standardauthentifizierung)", " integriert (Windows-Standardanmeldeinformationen)", + " sp (Dienstprinzipal)", " --token Wird mit --auth pat verwendet. Persönliches Zugriffstoken.", " --userName Wird mit „--auth negotiate“ oder „--auth alt“ verwendet. Geben Sie den Windows-Benutzer an.", " Name im Format: Domäne\\Benutzername oder userName@domain.com", @@ -275,6 +277,7 @@ "DirExpireLimit": "Ablauflimit des Verzeichnisses: {0} Tage.", "DiscoverBuildDir": "Veraltete Buildverzeichnisse ermitteln, die nicht mehr als {0} Tage verwendet wurden.", "DiscoverReleaseDir": "Ermitteln Sie veraltete Releaseverzeichnisse, die seit mehr als {0} Tagen nicht mehr verwendet wurden.", + "DockerCommandFinalExitCode": "Endgültiger Exitcode für {0}: {1}", "DownloadAgent": "{0}-Agent wird heruntergeladen.", "DownloadArtifactFinished": "Das Herunterladen des Artefakts wurde abgeschlossen.", "DownloadArtifacts": "Artefakte herunterladen", @@ -437,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "Zum Aufheben der Konfiguration eines Agents, der als Windows-Dienst ausgeführt wird, sind Administratorrechte erforderlich.", "NetworkServiceNotFound": "Das Netzwerkdienstkonto wurde nicht gefunden.", "NoArtifactsFound": "In der Version „{0}“ sind keine Artefakte verfügbar.", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "Der Vorgang erfordert {0}, das das Ende des Lebenszyklus erreicht hat. Dies wird durch die Organisationsrichtlinie blockiert. Bitte aktualisieren Sie den Vorgang auf Node20 oder Node24. So deaktivieren Sie diese Überprüfung vorübergehend: AGENT_RESTRICT_EOL_NODE_VERSIONS=false festlegen", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "Das {0}-Betriebssystem unterstützt {1} nicht. Stattdessen wird {0} verwendet. Bitte aktualisieren Sie das Betriebssystem, um mit zukünftigen Updates kompatibel zu bleiben.", + "NodeVersionNotAvailable": "Für die Hostausführung ist keine kompatible Node.js-Version verfügbar. Handlertyp: {0}. Dies kann vorkommen, wenn alle verfügbaren Versionen durch die EOL-Richtlinie blockiert sind. Aktualisieren Sie Ihre Pipeline, um Node20- oder Node24-Vorgänge zu verwenden. So deaktivieren Sie die EOL-Richtlinie vorübergehend: AGENT_RESTRICT_EOL_NODE_VERSIONS=false festlegen", "NoFolderToClean": "Der angegebene Reinigungsordner wurde nicht gefunden. Keine zu bereinigenden Elemente", "NoRestart": "Computer zu einem späteren Zeitpunkt neu starten? (J/N)", "NoRestartSuggestion": "Bei der Agent-Konfiguration wurde die automatische Anmeldung aktiviert. Starten Sie den Computer neu, damit die Einstellungen für die automatische Anmeldung wirksam werden.", @@ -509,6 +518,7 @@ "RestartMessage": "Starten Sie den Computer neu, um den Agent zu starten und damit die Einstellungen für die automatische Anmeldung wirksam werden.", "ReStreamLogsToFilesError": "Sie können \"--disableloguploads\" und \"--reStreamLogsToFiles\" nicht gleichzeitig verwenden.", "RetryCountLimitExceeded": "Die maximal zulässige Anzahl von Versuchen ist {0}. Es gab jedoch {1} Versuche. Die Anzahl der Wiederholungsversuche wird auf {0}verringert.", + "RetryingReplaceAgent": "Es wird erneut versucht, den Agenten zu ersetzen (Versuch {0} von {1}). Warten Sie {2} Sekunden vor dem nächsten Versuch …", "RMApiFailure": "API {0} ist einem Fehlercode {1} fehlgeschlagen", "RMArtifactContainerDetailsInvalidError": "Das Artefakt verfügt über keine gültigen Containerdetails: {0}", "RMArtifactContainerDetailsNotFoundError": "Das Artefakt enthält keine Containerdetails: {0}", @@ -681,7 +691,8 @@ "UnrecognizedCmdArgs": "Unbekannte Eingabeargumente für die Befehlszeile: „{0}“. Informationen zur Verwendung finden Sie unter: „.\\config.cmd --help“ oder „./config.sh --help“", "UnregisteringAgent": "Der Agent wird vom Server entfernt", "UnsupportedGitLfsVersion": "Ihre aktuelle Git-LFS-Version ist „{0}“; diese Version wird vom Agent nicht unterstützt. Führen Sie ein Upgrade auf mindestens Version „{1}“ durch. Weitere Informationen finden Sie unter „https://github.com/git-lfs/git-lfs/issues/3571“.", - "UnsupportedOsVersionByNet8": "Die Betriebssystemversion, auf der dieser Agent ausgeführt wird ({0}), wird bei einem bevorstehenden Update des Pipelines Agent nicht unterstützt. Unterstützte Betriebssystemversionen finden Sie unter https://aka.ms/azdo-pipeline-agent-net8.", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "Buildnummer aktualisieren", "UpdateBuildNumberForBuild": "Buildnummer für Build „{1}“ auf {0} aktualisieren", "UpdateInProgress": "Agent wird aktualisiert. Fahren Sie den Agent nicht herunter.", diff --git a/src/Misc/layoutbin/en-US/strings.json b/src/Misc/layoutbin/en-US/strings.json index 50a1d8d9a1..cecae36ac3 100644 --- a/src/Misc/layoutbin/en-US/strings.json +++ b/src/Misc/layoutbin/en-US/strings.json @@ -440,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "Needs Administrator privileges for unconfiguring agent that running as windows service.", "NetworkServiceNotFound": "Cannot find network service account", "NoArtifactsFound": "No artifacts are available in the version '{0}'.", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "Task requires {0} which has reached End-of-Life. This is blocked by organization policy. Please upgrade task to Node20 or Node24. To temporarily disable this check: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "The {0} operating system doesn't support {1}. Using {2} instead. Please upgrade the operating system to remain compatible with future updates.", + "NodeVersionNotAvailable": "No compatible Node.js version available for host execution. Handler type: {0}. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", "NoFolderToClean": "Specified cleaning folder was not found. Nothing to clean", "NoRestart": "Restart the machine at a later time? (Y/N)", "NoRestartSuggestion": "AutoLogon was enabled during agent configuration. It is recommended that the machine be restarted for AutoLogon settings to take effect.", @@ -685,7 +691,8 @@ "UnrecognizedCmdArgs": "Unrecognized command-line input arguments: '{0}'. For usage refer to: .\\config.cmd --help or ./config.sh --help", "UnregisteringAgent": "Removing agent from the server", "UnsupportedGitLfsVersion": "Your current Git LFS version is '{0}', which is unsupported by the agent. Please upgrade to at least version '{1}'. See https://github.com/git-lfs/git-lfs/issues/3571 for more details.", - "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported on an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "Update Build Number", "UpdateBuildNumberForBuild": "Update build number to {0} for build {1}", "UpdateInProgress": "Agent update in progress, do not shutdown agent.", diff --git a/src/Misc/layoutbin/es-ES/strings.json b/src/Misc/layoutbin/es-ES/strings.json index 75f72f7927..1779dc4728 100644 --- a/src/Misc/layoutbin/es-ES/strings.json +++ b/src/Misc/layoutbin/es-ES/strings.json @@ -10,6 +10,7 @@ "AddEnvironmentVMResourceTags": "¿Etiquetas de recursos de máquina virtual del entorno? (S/N)", "AgentAddedSuccessfully": "El agente se agregó correctamente", "AgentAlreadyInsideContainer": "No se admite la característica de contenedor cuando el agente ya se está ejecutando en un contenedor. Consulte la documentación (https://go.microsoft.com/fwlink/?linkid=875268).", + "AgentCdnAccessFailWarning": "Acción necesaria: el agente de Azure Pipelines no puede acceder a la nueva URL de CDN. Permite \"download.agent.dev.azure.com\" ahora para evitar fallos en las canalizaciones. Más información: https://devblogs.microsoft.com/devops/cdn-domain-url-change-for-agents-in-pipelines/", "AgentDoesNotSupportContainerFeatureRhel6": "El agente no admite la característica de contenedor en Red Hat Enterprise Linux 6 o CentOS 6.", "AgentDowngrade": "Cambiando el agente a una versión anterior. Esto suele deberse a una reversión del agente publicado actualmente para corregir un error. Para deshabilitar este comportamiento, establezca la variable de entorno AZP_AGENT_DOWNGRADE_DISABLED=true antes de iniciar el agente.", "AgentExit": "El agente se cerrará en breve para la actualización, debería volver a estar en línea en 10 segundos.", @@ -107,6 +108,7 @@ " negociar (Kerberos o NTLM)", " alt (Autenticación básica)", " integrado (credenciales predeterminadas de Windows)", + " sp (entidad de servicio)", " --token Se usa con --auth pat. Token de acceso personal.", " --userName Se usa con --auth negotiate o --auth alt. Especificar el usuario de Windows", " nombre con el formato: domain\\userName o userName@domain.com", @@ -275,6 +277,7 @@ "DirExpireLimit": "Límite de expiración del directorio: {0} días.", "DiscoverBuildDir": "Descubra directorios de compilación obsoletos que no se han usado durante más de {0} días.", "DiscoverReleaseDir": "Descubra directorios de versión obsoletos que no se han usado durante más de {0} días.", + "DockerCommandFinalExitCode": "Código de salida final para {0}: {1}", "DownloadAgent": "Descargando {0} agente", "DownloadArtifactFinished": "La descarga del artefacto ha finalizado.", "DownloadArtifacts": "Descargar artefactos", @@ -437,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "Necesita privilegios de administrador para quitar la configuración del agente que se ejecuta como servicio de Windows.", "NetworkServiceNotFound": "No se encuentra la cuenta de servicio de red", "NoArtifactsFound": "No hay artefactos disponibles en la versión \"{0}\".", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "La tarea requiere {0}, que ha llegado al final de su vida útil. Esto está bloqueado por la directiva de la organización. Actualice la tarea a Node20 o Node24. Para desactivar temporalmente esta comprobación: establezca AGENT_RESTRICT_EOL_NODE_VERSIONS=false", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "El sistema operativo {0} no es compatible con {1}. Use {2} en su lugar. Actualice el sistema operativo para mantener la compatibilidad con futuras actualizaciones.", + "NodeVersionNotAvailable": "No hay ninguna versión compatible de Node.js disponible para la ejecución en el host. Tipo de controlador: {0}. Esto puede ocurrir si la directiva EOL bloquea todas las versiones disponibles. Actualice la canalización para usar tareas de Node20 o Node24. Para desactivar temporalmente la directiva EOL: establezca AGENT_RESTRICT_EOL_NODE_VERSIONS=false", "NoFolderToClean": "No se ha encontrado la carpeta de limpieza especificada. No hay nada que limpiar", "NoRestart": "¿Quiere reiniciar la máquina más tarde? (S/N)", "NoRestartSuggestion": "Se ha habilitado el inicio de sesión automático durante la configuración del agente. Se recomienda reiniciar la máquina para que surta efecto la configuración de inicio de sesión automático.", @@ -509,6 +518,7 @@ "RestartMessage": "Reinicie la máquina para iniciar el agente y para que la configuración de inicio de sesión automático surta efecto.", "ReStreamLogsToFilesError": "No puede usar --disableloguploads y --reStreamLogsToFiles al mismo tiempo.", "RetryCountLimitExceeded": "El número máximo permitido de intentos es {0} pero se obtuvo {1}. El número de reintentos se disminuirá a {0}.", + "RetryingReplaceAgent": "Reintentar reemplazar al agente (intento {0} de {1}). Esperando {2} segundos antes del siguiente intento...", "RMApiFailure": "Error de API {0} con un código de error {1}", "RMArtifactContainerDetailsInvalidError": "El artefacto no tiene detalles de contenedor válidos: {0}", "RMArtifactContainerDetailsNotFoundError": "El artefacto no contiene detalles del contenedor: {0}", @@ -681,7 +691,8 @@ "UnrecognizedCmdArgs": "Argumentos de entrada de la línea de comandos no reconocidos: \"{0}\". Para obtener información sobre el uso, consulte: .\\config.cmd --help o ./config.sh --help", "UnregisteringAgent": "Eliminando agente del servidor", "UnsupportedGitLfsVersion": "Su versión actual de Git LFS es \"{0}\", que no es compatible con el agente. Actualice al menos a la versión \"{1}\". Consulte https://github.com/git-lfs/git-lfs/issues/3571 para obtener más detalles.", - "UnsupportedOsVersionByNet8": "La versión del sistema operativo en la que se está ejecutando este agente ({0}) no se admite en una próxima actualización del agente de pipelines. Para ver las versiones de sistema operativo compatibles, consulte https://aka.ms/azdo-pipeline-agent-net8.", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "Actualizar número de compilación", "UpdateBuildNumberForBuild": "Actualice el número de compilación a {0} para la compilación {1}", "UpdateInProgress": "Actualización del agente en curso, no apague el agente.", diff --git a/src/Misc/layoutbin/fr-FR/strings.json b/src/Misc/layoutbin/fr-FR/strings.json index 507571dd8a..06ada08f93 100644 --- a/src/Misc/layoutbin/fr-FR/strings.json +++ b/src/Misc/layoutbin/fr-FR/strings.json @@ -10,6 +10,7 @@ "AddEnvironmentVMResourceTags": "Étiquettes de ressources de la machine virtuelle d'environnement ? (O/N)", "AgentAddedSuccessfully": "L’agent a été ajouté", "AgentAlreadyInsideContainer": "La fonctionnalité de conteneur n'est pas prise en charge quand l'agent est déjà en cours d'exécution dans le conteneur. Consultez la documentation de référence (https://go.microsoft.com/fwlink/?linkid=875268)", + "AgentCdnAccessFailWarning": "Action requise : L’agent Azure Pipelines ne peut pas atteindre la nouvelle URL CDN. Ajoutez « download.agent.dev.azure.com » à la liste d’autorisation dès maintenant pour éviter les échecs du pipeline. Détails : https://devblogs.microsoft.com/devops/cdn-domain-url-change-for-agents-in-pipelines/", "AgentDoesNotSupportContainerFeatureRhel6": "L'agent ne prend pas en charge la fonction de conteneur sur Red Hat Enterprise Linux 6 ou CentOS 6.", "AgentDowngrade": "Passage de l'agent à une version antérieure. Cela est généralement dû à une restauration de l'agent publié pour une résolution de bogue. Pour désactiver ce comportement, définissez la variable d'environnement AZP_AGENT_DOWNGRADE_DISABLED=true avant de lancer votre agent.", "AgentExit": "L’agent va se fermer sous peu pour la mise à jour, doit être de nouveau en ligne dans un délai de 10 secondes.", @@ -107,6 +108,7 @@ " négocier (Kerberos ou NTLM)", " alt (authentification de base)", " intégré (informations d’identification Windows par défaut)", + " sp (principal du service)", " --token Utilisé avec --auth pat. Jeton d’accès personnel.", " --userName Utilisé avec --auth negotiate ou --auth alt. Spécifier l’utilisateur Windows", " nom au format : domaine\\userName ou userName@domain.com", @@ -275,6 +277,7 @@ "DirExpireLimit": "Limite d’expiration du répertoire : {0} jours.", "DiscoverBuildDir": "Découvrez les répertoires de versions périmés qui n'ont pas été utilisés pendant plus de {0} jours.", "DiscoverReleaseDir": "Découvrez les répertoires de version périmés qui n'ont pas été utilisés pendant plus de {0} jours.", + "DockerCommandFinalExitCode": "Code de sortie final pour {0}: {1}", "DownloadAgent": "Téléchargement de l’agent {0}", "DownloadArtifactFinished": "Fin du téléchargement de l'artefact.", "DownloadArtifacts": "Télécharger les artefacts", @@ -437,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "Des privilèges d’administrateur sont nécessaires pour annuler la configuration de l’agent s’exécutant en tant que service Windows.", "NetworkServiceNotFound": "Compte de service réseau introuvable", "NoArtifactsFound": "Aucun artefact n’est disponible dans la version '{0}'.", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "La tâche nécessite {0} qui a atteint sa fin de service. Cette opération est bloquée par la politique de l’entreprise. Mettez à niveau la tâche vers Node20 ou Node24. Pour désactiver temporairement cette vérification : définissez AGENT_RESTRICT_EOL_NODE_VERSIONS=false", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "Le système d’exploitation {0} ne prend pas en charge {1}. Utilisation de {2} à la place. Mettez à niveau le système d’exploitation pour qu’il reste compatible avec les futures mises à jour.", + "NodeVersionNotAvailable": "Aucune version compatible de Node.js n’est disponible pour l’exécution de l’hôte. Type de gestionnaire : {0}. Cela peut se produire si toutes les versions disponibles sont bloquées par la politique de fin de service (EOL). Mettez à jour votre pipeline pour utiliser les tâches Node20 ou Node24. Pour désactiver temporairement la politique EOL : définissez AGENT_RESTRICT_EOL_NODE_VERSIONS=false", "NoFolderToClean": "Le dossier de nettoyage spécifié est introuvable. Rien à nettoyer", "NoRestart": "Redémarrer la machine plus tard ? (O/N)", "NoRestartSuggestion": "L'ouverture de session automatique a été activée durant la configuration de l'agent. Il est recommandé de redémarrer la machine pour que les paramètres d'ouverture de session automatique soient pris en compte.", @@ -509,6 +518,7 @@ "RestartMessage": "Redémarrez la machine pour lancer l’agent et pour que les paramètres d’ouverture de session automatique prennent effet.", "ReStreamLogsToFilesError": "Vous ne pouvez pas utiliser --disableloguploads et --reStreamLogsToFiles en même temps !", "RetryCountLimitExceeded": "Le nombre maximal autorisé de tentatives est {0} mais a obtenu {1}. Le nombre de tentatives sera réduit à {0}.", + "RetryingReplaceAgent": "Nouvelle tentative de remplacement de l’agent (tentative {0} de {1}). Attendez {2} secondes avant la prochaine tentative...", "RMApiFailure": "Échec de l’API {0} avec le code d’erreur {1}", "RMArtifactContainerDetailsInvalidError": "L’artefact n’a pas de détails de conteneur valides : {0}", "RMArtifactContainerDetailsNotFoundError": "L’artefact ne contient pas les détails du conteneur : {0}", @@ -681,7 +691,8 @@ "UnrecognizedCmdArgs": "Arguments d’entrée de ligne de commande non reconnus : « {0} ». Pour plus d’informations sur l’utilisation, consultez : .\\config.cmd --help ou ./config.sh --help", "UnregisteringAgent": "Suppression de l’agent du serveur", "UnsupportedGitLfsVersion": "Votre version actuelle de Git LFS est la version « {0} », et n'est pas prise en charge par l'agent. Effectuez une mise à niveau au moins vers la version « {1} ». Pour plus d'informations, consultez https://github.com/git-lfs/git-lfs/issues/3571.", - "UnsupportedOsVersionByNet8": "La version du système d’exploitation sur laquelle cet agent s’exécute ({0}) n’est pas prise en charge lors d’une mise à jour à venir de l’agent pipelines. Pour connaître les versions de système d’exploitation prises en charge, voir https://aka.ms/azdo-pipeline-agent-net8.", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "Mettre à jour le numéro de build", "UpdateBuildNumberForBuild": "Mettre à jour le numéro de build à {0} pour la build {1}", "UpdateInProgress": "Mise à jour de l’agent en cours, ne pas arrêter l’agent.", diff --git a/src/Misc/layoutbin/it-IT/strings.json b/src/Misc/layoutbin/it-IT/strings.json index bccadeecb9..3e1b189739 100644 --- a/src/Misc/layoutbin/it-IT/strings.json +++ b/src/Misc/layoutbin/it-IT/strings.json @@ -10,6 +10,7 @@ "AddEnvironmentVMResourceTags": "Tag per risorse di tipo Macchina virtuale ambiente? (S/N)", "AgentAddedSuccessfully": "L'agente è stato aggiunto", "AgentAlreadyInsideContainer": "La funzione del contenitore non è supportata quando l'agente è già in esecuzione all'interno del contenitore. Vedere la documentazione (https://go.microsoft.com/fwlink/?linkid=875268)", + "AgentCdnAccessFailWarning": "Azione richiesta: l'agente di Azure Pipelines non riesce a raggiungere il nuovo URL della rete CDN. Aggiungi ora 'download.agent.dev.azure.com' all'elenco degli elementi consentiti per prevenire errori nelle pipeline. Dettagli: https://devblogs.microsoft.com/devops/cdn-domain-url-change-for-agents-in-pipelines/", "AgentDoesNotSupportContainerFeatureRhel6": "L'agente non supporta la funzionalità dei contenitori in Red Hat Enterprise Linux 6 o CentOS 6.", "AgentDowngrade": "Verrà effettuato il downgrade dell'agente a una versione precedente. Questo comportamento è in genere causato dal ripristino dello stato precedente dell'agente attualmente pubblicato per applicare una correzione di bug. Per disabilitarlo, impostare la variabile di ambiente AZP_AGENT_DOWNGRADE_DISABLED=true prima di avviare l'agente.", "AgentExit": "L'agente verrà chiuso a breve per l'aggiornamento. Dovrebbe tornare online entro 10 secondi.", @@ -107,6 +108,7 @@ " negoziare (Kerberos o NTLM)", " alt (autenticazione di base)", " integrata (credenziali predefinite di Windows)", + " sp (entità servizio)", " --token Usato con --auth pat. Token di accesso personale.", " --userName Usato con --auth negotiate o --auth alt. Specificare l'utente Windows", " nome nel formato: dominio\\nomeutente o userName@domain.com", @@ -275,6 +277,7 @@ "DirExpireLimit": "Limite di scadenza della directory: {0} giorni.", "DiscoverBuildDir": "Rileva le directory di compilazione non aggiornate che non sono state usate per più di {0} giorni.", "DiscoverReleaseDir": "Rileva le directory di versione non aggiornate che non sono state usate per più di {0} giorni.", + "DockerCommandFinalExitCode": "Codice di uscita finale per {0}: {1}", "DownloadAgent": "Download dell'agente {0}", "DownloadArtifactFinished": "Il download dell'artefatto è stato completato.", "DownloadArtifacts": "Scarica artefatti", @@ -437,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "Per annullare la configurazione dell'agente in esecuzione come servizio di Windows, sono necessari i privilegi di amministratore.", "NetworkServiceNotFound": "Impossibile trovare l'account del servizio di rete", "NoArtifactsFound": "Non sono disponibili artefatti nella versione '{0}'.", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "L'attività richiede {0} che ha raggiunto la fine del servizio. Questa operazione è bloccata dal criterio dell'organizzazione. Aggiornare l'attività a Node20 o Node24. Per disabilitare temporaneamente questo criterio: impostare AGENT_RESTRICT_EOL_NODE_VERSIONS=false", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "Il sistema operativo {0} non supporta {1}. Provare a usare {2}. Aggiornare il sistema operativo per mantenere la compatibilità con gli aggiornamenti futuri.", + "NodeVersionNotAvailable": "Nessuna versione compatibile di Node.js disponibile per l'esecuzione sull'host. Tipo di gestore: {0}. Questo può accadere se tutte le versioni disponibili sono bloccate dal criterio EOL. Aggiornare la pipeline per usare le attività Node20 o Node24. Per disabilitare temporaneamente il criterio EOL: impostare AGENT_RESTRICT_EOL_NODE_VERSIONS=false", "NoFolderToClean": "La cartella di pulizia specificata non è stata trovata. Nulla da pulire", "NoRestart": "Riavviare il computer in un secondo momento? (S/N)", "NoRestartSuggestion": "Durante la configurazione dell'agente è stato abilitato l'accesso automatico. È consigliabile riavviare il computer per rendere effettive le impostazioni di accesso automatico.", @@ -509,6 +518,7 @@ "RestartMessage": "Riavviare il computer per avviare l'agente e rendere effettive le impostazioni di accesso automatico.", "ReStreamLogsToFilesError": "Non è possibile usare --disableloguploads e --reStreamLogsToFiles contemporaneamente.", "RetryCountLimitExceeded": "Il numero massimo consentito di tentativi è {0}, ma è stato ottenuto {1}. Il numero di tentativi verrà ridotto a {0}.", + "RetryingReplaceAgent": "Nuovo tentativo di sostituzione dell'agente (tentativo {0} di {1}). In attesa di {2} secondi prima del prossimo tentativo...", "RMApiFailure": "API {0} non riuscita con codice di errore {1}", "RMArtifactContainerDetailsInvalidError": "L'artefatto non contiene dettagli del contenitore valido: {0}", "RMArtifactContainerDetailsNotFoundError": "L'artefatto non contiene i dettagli del contenitore: {0}", @@ -681,7 +691,8 @@ "UnrecognizedCmdArgs": "Argomenti di input della riga di comando non riconosciuti: '{0}'. Per informazioni sull'utilizzo, vedere. .\\config.cmd --help o ./config.sh --help", "UnregisteringAgent": "Rimozione dell'agente dal server", "UnsupportedGitLfsVersion": "La versione LFS corrente di GIT è '{0}', ma non è supportata dall'agente. Eseguire l'aggiornamento almeno alla versione '{1}'. Per maggiori dettagli, vedere https://github.com/git-lfs/git-lfs/issues/3571.", - "UnsupportedOsVersionByNet8": "La versione del sistema operativo in cui è in esecuzione l'agente ({0}) non è supportata in un prossimo aggiornamento dell'agente Pipelines. Per le versioni del sistema operativo supportate, vedere https://aka.ms/azdo-pipeline-agent-net8.", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "Aggiorna il numero di build", "UpdateBuildNumberForBuild": "Aggiornare il numero di build in {0} per la compilazione {1}", "UpdateInProgress": "Aggiornamento dell'agente in corso. Non arrestare l'agente.", diff --git a/src/Misc/layoutbin/ja-JP/strings.json b/src/Misc/layoutbin/ja-JP/strings.json index 49c4e98044..c7f80cee80 100644 --- a/src/Misc/layoutbin/ja-JP/strings.json +++ b/src/Misc/layoutbin/ja-JP/strings.json @@ -10,6 +10,7 @@ "AddEnvironmentVMResourceTags": "環境の仮想マシン リソース タグですか? (Y/N)", "AgentAddedSuccessfully": "ユーザーが正常に追加されました。", "AgentAlreadyInsideContainer": "エージェントが既にコンテナー内で実行されている場合、コンテナーの機能はサポートされません。ドキュメント (https://go.microsoft.com/fwlink/?linkid=875268) を参照してください", + "AgentCdnAccessFailWarning": "操作が必要です: Azure Pipelines エージェントが新しい CDN URL に到達できません。パイプラインの失敗を防ぐため、今すぐ 'download.agent.dev.azure.com' を許可リストに追加してください。詳細情報: https://devblogs.microsoft.com/devops/cdn-domain-url-change-for-agents-in-pipelines/", "AgentDoesNotSupportContainerFeatureRhel6": "エージェントは、Red Hat Enterprise Linux 6 または CentOS 6 のコンテナー機能をサポートしていません。", "AgentDowngrade": "エージェントを下位バージョンにダウングレードします。これは通常、バグ修正のために現在発行されているエージェントのロールバックが原因です。この動作を無効にするには、エージェントを起動する前に環境変数 AZP_AGENT_DOWNGRADE_DISABLED=true を設定します。", "AgentExit": "エージェントは間もなく更新のために終了します。10 秒以内にオンラインに戻ります。", @@ -107,6 +108,7 @@ " ネゴシエート (Kerberos または NTLM)", " alt (Basic 認証)", " 統合 (Windows の既定の資格情報)", + "sp (サービス プリンシパル)", " --token --auth pat と共に使用します。個人用アクセス トークン。", " --userName --auth negotiate または --auth alt と共に使用されます。Windows ユーザーの指定", " 以下の形式で命名します: domain\\userName または userName@domain.com", @@ -275,6 +277,7 @@ "DirExpireLimit": "ディレクトリの有効期限: {0}日。", "DiscoverBuildDir": "{0} 日以上使用されていない古いビルド ディレクトリを検出します。", "DiscoverReleaseDir": "{0} 日以上使用されていない古いリリース ディレクトリを検出します。", + "DockerCommandFinalExitCode": "{0} の最終終了コード: {1}", "DownloadAgent": "{0} エージェントをダウンロードしています", "DownloadArtifactFinished": "成果物のダウンロードが完了しました。", "DownloadArtifacts": "成果物のダウンロード", @@ -437,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "Windows サービスとして実行されているエージェントを構成解除するための管理者特権が必要です。", "NetworkServiceNotFound": "ネットワーク サービス アカウントが見つかりません", "NoArtifactsFound": "バージョン '{0}' で使用できる成果物がありません。", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "タスクにはサポート終了になった {0} が必要です。これは組織のポリシーでブロックされています。タスクを Node20 または Node24 にアップグレードしてください。このチェックを一時的に無効にするには、AGENT_RESTRICT_EOL_NODE_VERSIONS=false に設定します", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "{0} オペレーティング システムでは、{1} はサポートされていません。代わりに {2} を使用しています。今後の更新プログラムとの互換性を維持するには、オペレーティング システムをアップグレードしてください。", + "NodeVersionNotAvailable": "ホストの実行に使用できる互換性のある Node.js バージョンがありません。ハンドラーの種類: {0}。これは、使用可能なすべてのバージョンが EOL ポリシーによってブロックされている場合に発生することがあります。Node20 または Node24 のタスクを使用するようにパイプラインを更新してください。EOL ポリシーを一時的に無効にするには、AGENT_RESTRICT_EOL_NODE_VERSIONS=false に設定します", "NoFolderToClean": "指定されたクリーンアップ対象フォルダーが見つかりませんでした。クリーンアップするものがありません", "NoRestart": "後でマシンを再起動しますか? (Y/N)", "NoRestartSuggestion": "エージェントの構成中に AutoLogon が有効になりました。AutoLogon の設定を有効にするには、コンピューターを再起動することをお勧めします。", @@ -509,6 +518,7 @@ "RestartMessage": "マシンを再起動してエージェントを起動し、自動ログオン設定を有効にします。", "ReStreamLogsToFilesError": "--disableloguploads と --reStreamLogsToFiles を同時に使用することはできません。", "RetryCountLimitExceeded": "許可されている最大試行回数は {0} ですが、{1} を取得しました。再試行回数が {0} に減ります。", + "RetryingReplaceAgent": "エージェントの置換を再試行しています (試行 {0}/{1})。次の試行までの待機時間、{2} 秒...", "RMApiFailure": "API {0} がエラー コード {1} で失敗しました", "RMArtifactContainerDetailsInvalidError": "成果物に有効なコンテナーの詳細がありません: {0}", "RMArtifactContainerDetailsNotFoundError": "成果物にコンテナーの詳細が含まれていません: {0}", @@ -681,7 +691,8 @@ "UnrecognizedCmdArgs": "認識されないコマンド ライン入力引数: '{0}'。使用法については、.\\config.cmd --help または ./config.sh --help を参照してください。", "UnregisteringAgent": "サーバーからエージェントを削除しています", "UnsupportedGitLfsVersion": "現在の Git LFS バージョンは '{0}' で、エージェントではサポートされていません。少なくともバージョン '{1}' にアップグレードしてください。詳細については、「https://github.com/git-lfs/git-lfs/issues/3571」を参照してください。", - "UnsupportedOsVersionByNet8": "このエージェントが実行されているオペレーティング システムのバージョン ({0}) は、Pipelines エージェントの今後の更新ではサポートされていません。サポートされているオペレーティング システムのバージョンについては、「https://aka.ms/azdo-pipeline-agent-net8」を参照してください。", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "ビルド番号の更新", "UpdateBuildNumberForBuild": "ビルド {1} のビルド番号を {0}に更新する", "UpdateInProgress": "エージェントの更新進行中、エージェントをシャットダウンしないでください。", diff --git a/src/Misc/layoutbin/ko-KR/strings.json b/src/Misc/layoutbin/ko-KR/strings.json index 1d21eb595c..fff1e3f56a 100644 --- a/src/Misc/layoutbin/ko-KR/strings.json +++ b/src/Misc/layoutbin/ko-KR/strings.json @@ -10,6 +10,7 @@ "AddEnvironmentVMResourceTags": "환경 가상 머신 리소스 태그입니까? (예/아니요)", "AgentAddedSuccessfully": "에이전트를 추가했습니다.", "AgentAlreadyInsideContainer": "컨테이너 내에서 에이전트가 이미 실행 중인 경우 컨테이너 기능은 지원되지 않습니다. 설명서(https://go.microsoft.com/fwlink/?linkid=875268)를 참조하세요.", + "AgentCdnAccessFailWarning": "작업 필요: Azure Pipelines 에이전트가 새 CDN URL에 연결할 수 없습니다. 이제 파이프라인 오류를 방지하려면 'download.agent.dev.azure.com'을 허용 목록에 추가하세요. 세부 정보: https://devblogs.microsoft.com/devops/cdn-domain-url-change-for-agents-in-pipelines/", "AgentDoesNotSupportContainerFeatureRhel6": "에이전트는 Red Hat Enterprise Linux 6 또는 CentOS 6의 컨테이너 기능을 지원하지 않습니다.", "AgentDowngrade": "에이전트를 낮은 버전으로 다운그레이드합니다. 이 오류는 일반적으로 버그 수정에 대해 현재 게시된 에이전트의 롤백 때문에 발생합니다. 이 동작을 사용하지 않도록 설정하려면 에이전트를 시작하기 전에 환경 변수 AZP_AGENT_DOWNGRADE_DISABLED=true로 설정하세요.", "AgentExit": "에이전트가 업데이트를 위해 곧 종료되며 10초 이내에 다시 온라인 상태가 되어야 합니다.", @@ -107,9 +108,10 @@ " negotiate (Kerberos 또는 NTLM)", " alt (기본 인증)", " integrated (Windows 기본 자격 증명)", + " sp(서비스 주체)", " --token --auth pat과 함께 사용됩니다. 개인용 액세스 토큰입니다.", " --userName --auth 협상 또는 --auth alt와 함께 사용됩니다. Windows 사용자를 지정합니다", - " 다음 형식의 이름: domain\\userName 또는 userName@domain.com", + " 이름을 domain\\userName 또는 userName@domain.com 형식으로 지정합니다.", " --password --auth 협상 또는 --auth Alt와 함께 사용됩니다.", " --unattended 무인 구성. 메시지가 표시되지 않습니다. 모든 답변은 다음을 충족해야 합니다", " 명령줄에 제공됩니다.", @@ -275,6 +277,7 @@ "DirExpireLimit": "디렉터리 만료 제한: {0}일.", "DiscoverBuildDir": "{0}일 이상 사용되지 않은 오래된 빌드 디렉터리를 검색합니다.", "DiscoverReleaseDir": "{0}일 이상 사용되지 않은 오래된 릴리스 디렉터리를 검색합니다.", + "DockerCommandFinalExitCode": "{0}에 대한 최종 종료 코드: {1}", "DownloadAgent": "{0} 에이전트 다운로드 중", "DownloadArtifactFinished": "아티팩트 다운로드가 완료되었습니다.", "DownloadArtifacts": "아티팩트 다운로드", @@ -437,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "Windows 서비스로 실행되는 에이전트를 구성 해제하려면 관리자 권한이 필요합니다.", "NetworkServiceNotFound": "네트워크 서비스 계정을 찾을 수 없습니다", "NoArtifactsFound": "버전 '{0}'에서 사용할 수 있는 아티팩트가 없습니다.", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "작업에 수명이 종료된 {0}이(가) 필요합니다. 조직 정책에 의해 차단되었습니다. 작업을 Node20 또는 Node24로 업그레이드하세요. 이 검사를 일시적으로 사용하지 않도록 설정하려면 AGENT_RESTRICT_EOL_NODE_VERSIONS=false로 설정합니다.", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "{0} 운영 체제가 {1}을(를) 지원하지 않습니다. 대신 {2}을(를) 사용하세요. 향후 업데이트와의 호환성을 유지하려면 운영 체제를 업그레이드하세요.", + "NodeVersionNotAvailable": "호스트 실행에 사용할 수 있는 호환되는 Node.js 버전이 없습니다. 처리기 유형: {0}. 사용 가능한 모든 버전이 EOL 정책에 의해 차단되는 경우 발생할 수 있습니다. Node20 또는 Node24 작업을 사용하도록 파이프라인을 업데이트하세요. EOL 정책을 일시적으로 사용하지 않도록 설정하려면 AGENT_RESTRICT_EOL_NODE_VERSIONS=false로 설정합니다.", "NoFolderToClean": "지정한 정리 폴더를 찾을 수 없습니다. 정리할 항목 없음", "NoRestart": "나중에 컴퓨터를 다시 시작하시겠습니까? (Y/N)", "NoRestartSuggestion": "에이전트를 구성하는 동안 자동 로그온이 활성화되었습니다. 자동 로그온 설정을 적용하려면 컴퓨터를 다시 시작하는 것이 좋습니다.", @@ -509,6 +518,7 @@ "RestartMessage": "에이전트를 시작하고 자동 로그온 설정을 적용하려면 머신을 다시 시작하세요.", "ReStreamLogsToFilesError": "--disableloguploads 및 --reStreamLogsToFiles를 동시에 사용할 수 없습니다.", "RetryCountLimitExceeded": "허용되는 최대 시도 횟수는 {0}이지만 {1}회를 받았습니다. 다시 시도 횟수는 {0}회로 줄어듭니다.", + "RetryingReplaceAgent": "에이전트 교체 다시 시도 중({0}/{1} 시도). 다음 시도 전까지 {2}초 대기 중...", "RMApiFailure": "API {0}이(가) 오류 코드 {1}과(와) 함께 실패했습니다.", "RMArtifactContainerDetailsInvalidError": "아티팩트에 유효한 컨테이너 세부 정보가 없습니다. {0}", "RMArtifactContainerDetailsNotFoundError": "아티팩트에 컨테이너 세부 정보가 포함되어 있지 않습니다. {0}", @@ -681,7 +691,8 @@ "UnrecognizedCmdArgs": "인식할 수 없는 명령줄 입력 인수: '{0}'. 사용법은 .\\config.cmd --help 또는 ./config.sh --help를 참조하세요.", "UnregisteringAgent": "서버에서 에이전트 제거", "UnsupportedGitLfsVersion": "현재 Git LFS 버전은 에이전트에서 지원하지 않는 '{0}'입니다. 최소한 '{1}' 버전으로 업그레이드하세요. 자세한 내용은 https://github.com/git-lfs/git-lfs/issues/3571을 참조하세요.", - "UnsupportedOsVersionByNet8": "이 에이전트가 실행 중인 운영 체제 버전({0})은 파이프라인 에이전트에 대한 향후 업데이트에서 지원되지 않습니다. 지원되는 운영 체제 버전은 https://aka.ms/azdo-pipeline-agent-net8을 참조하세요.", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "빌드 번호 업데이트", "UpdateBuildNumberForBuild": "빌드 {1}의 빌드 번호를 {0}(으)로 업데이트", "UpdateInProgress": "에이전트 업데이트가 진행 중입니다. 에이전트를 종료하지 마세요.", diff --git a/src/Misc/layoutbin/powershell/Add-MSBuildCapabilities.ps1 b/src/Misc/layoutbin/powershell/Add-MSBuildCapabilities.ps1 index 68a6524724..4855b74bd7 100644 --- a/src/Misc/layoutbin/powershell/Add-MSBuildCapabilities.ps1 +++ b/src/Misc/layoutbin/powershell/Add-MSBuildCapabilities.ps1 @@ -65,6 +65,8 @@ Get-MSBuildCapabilities -MajorVersion 16 Get-MSBuildCapabilities -MajorVersion 17 +Get-MSBuildCapabilities -MajorVersion 18 + # Add 64-bit. $latest = $null $null = Add-CapabilityFromRegistry -Name "MSBuild_2.0_x64" -Hive 'LocalMachine' -View 'Registry64' -KeyName $keyName20 -ValueName 'MSBuildToolsPath' -Value ([ref]$latest) @@ -85,3 +87,5 @@ if ($vs15 -and $vs15.installationPath) { Get-MSBuildCapabilities -MajorVersion 16 -Add_x64 Get-MSBuildCapabilities -MajorVersion 17 -Add_x64 + +Get-MSBuildCapabilities -MajorVersion 18 -Add_x64 diff --git a/src/Misc/layoutbin/powershell/Add-VisualStudioCapabilities.ps1 b/src/Misc/layoutbin/powershell/Add-VisualStudioCapabilities.ps1 index ac85163eb1..80aaf6fddc 100644 --- a/src/Misc/layoutbin/powershell/Add-VisualStudioCapabilities.ps1 +++ b/src/Misc/layoutbin/powershell/Add-VisualStudioCapabilities.ps1 @@ -30,7 +30,7 @@ function Add-TestCapability { function Get-VSCapabilities { param ( [Parameter(Mandatory = $true)] - [ValidateSet(15, 16, 17)] + [ValidateSet(15, 16, 17, 18)] [int]$MajorVersion, [Parameter(Mandatory = $true)] @@ -87,6 +87,7 @@ $keyName14 = 'Software\Microsoft\VisualStudio\14.0' $keyName15 = 'Software\Microsoft\VisualStudio\15.0' $keyName16 = 'Software\Microsoft\VisualStudio\16.0' $keyName17 = 'Software\Microsoft\VisualStudio\17.0' +$keyName18 = 'Software\Microsoft\VisualStudio\18.0' # Add the capabilities. $latestVS = $null @@ -111,3 +112,5 @@ Get-VSCapabilities -MajorVersion 15 -keyName $keyName15 Get-VSCapabilities -MajorVersion 16 -keyName $keyName16 Get-VSCapabilities -MajorVersion 17 -keyName $keyName17 + +Get-VSCapabilities -MajorVersion 18 -keyName $keyName18 diff --git a/src/Misc/layoutbin/powershell/CapabilityHelpers/VisualStudioFunctions.ps1 b/src/Misc/layoutbin/powershell/CapabilityHelpers/VisualStudioFunctions.ps1 index b467f6512d..33efdd067d 100644 --- a/src/Misc/layoutbin/powershell/CapabilityHelpers/VisualStudioFunctions.ps1 +++ b/src/Misc/layoutbin/powershell/CapabilityHelpers/VisualStudioFunctions.ps1 @@ -2,14 +2,14 @@ function Get-VisualStudio { [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [ValidateSet(15, 16, 17)] + [ValidateSet(15, 16, 17, 18)] [int]$MajorVersion ) try { - # Query for the latest 15.*/16.*/17.* version. + # Query for the latest 15.*/16.*/17.*/18.* version. # - # Note, the capability is registered as VisualStudio_15.0/VisualStudio_16.0/VisualStudio_17.0 however the actual + # Note, the capability is registered as VisualStudio_15.0/VisualStudio_16.0/VisualStudio_17.0/VisualStudio_18.0 however the actual # version may something like 15.2/16.2. $preReleaseFlag = [string]::Empty; if ($env:IncludePrereleaseVersions -eq $true) diff --git a/src/Misc/layoutbin/ru-RU/strings.json b/src/Misc/layoutbin/ru-RU/strings.json index b5f5c5b8ed..61bc032c94 100644 --- a/src/Misc/layoutbin/ru-RU/strings.json +++ b/src/Misc/layoutbin/ru-RU/strings.json @@ -10,6 +10,7 @@ "AddEnvironmentVMResourceTags": "Теги ресурсов виртуальной машины среды? (Да/Нет)", "AgentAddedSuccessfully": "Агент успешно добавлен", "AgentAlreadyInsideContainer": "Функция контейнера не поддерживается, если агент уже работает в контейнере. Обратитесь к документации (https://go.microsoft.com/fwlink/?linkid=875268).", + "AgentCdnAccessFailWarning": "Требуется действие: агенту Azure Pipelines не удается получить доступ к новому URL-адресу CDN. Добавьте \"download.agent.dev.azure.com\" в список разрешений, чтобы предотвратить сбои в работе конвейера. Сведения: https://devblogs.microsoft.com/devops/cdn-domain-url-change-for-agents-in-pipelines/", "AgentDoesNotSupportContainerFeatureRhel6": "Агент не поддерживает функцию контейнера в Red Hat Enterprise Linux 6 или CentOS 6.", "AgentDowngrade": "Понижение версии агента до более ранней. Обычно это обусловлено откатом текущего опубликованного агента для исправления ошибки. Чтобы отключить это поведение, задайте для переменной среды AZP_AGENT_DOWNGRADE_DISABLED значение true перед запуском агента.", "AgentExit": "Агент скоро завершит работу для обновления. Он должен снова заработать в течение 10 секунд.", @@ -107,6 +108,7 @@ " согласование (Kerberos или NTLM)", " alt (базовая проверка подлинности)", " интегрированные (учетные данные Windows по умолчанию)", + " sp (субъект-служба)", " --token Используется с --auth pat. Токен личного доступа.", " --userName <имя_пользователя> Используется с --auth negotiate или --auth alt. Укажите имя пользователя Windows", " имя в формате: домен\\имя_пользователя или имя_пользователя@домен.com", @@ -275,6 +277,7 @@ "DirExpireLimit": "Окончание срока действия каталога: {0} дней.", "DiscoverBuildDir": "Обнаружение устаревших каталогов сборки, которые не использовались дольше {0} дн.", "DiscoverReleaseDir": "Обнаружение устаревших каталогов выпуска, которые не использовались дольше {0} дн.", + "DockerCommandFinalExitCode": "Окончательный код завершения для {0}: {1}", "DownloadAgent": "Загрузка агента {0}", "DownloadArtifactFinished": "Скачивание артефакта завершено.", "DownloadArtifacts": "Скачать артефакты", @@ -437,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "Для отмены настройки агента, запущенного как служба Windows, требуются права администратора.", "NetworkServiceNotFound": "Не удалось найти учетную запись сетевой службы", "NoArtifactsFound": "Нет доступных артефактов в версии ''{0}''.", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "Задача требует {0}, для которого наступило окончание жизненного цикла. Это заблокировано политикой организации. Обновите задачу до Node20 или Node24. Чтобы временно отключить эту проверку, установите значение AGENT_RESTRICT_EOL_NODE_VERSIONS=false", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "Операционная система {0} не поддерживает {1}. Вместо этого используется {2}. Повысьте статус операционной системы, чтобы сохранить совместимость с будущими обновлениями.", + "NodeVersionNotAvailable": "Нет доступной совместимой версии Node.js для выполнения узла. Тип обработчика: {0}. Это может произойти, если все доступные версии заблокированы политикой EOL. Обновите конвейер, чтобы использовать задачи Node20 или Node24. Чтобы временно отключить политику EOL, установите значение AGENT_RESTRICT_EOL_NODE_VERSIONS=false", "NoFolderToClean": "Указанная папка очистки не найдена. Нет элементов для очистки", "NoRestart": "Перезагрузить компьютер позже? (Да/Нет)", "NoRestartSuggestion": "Во время настройки агента был включен автоматический вход в систему. Рекомендуется перезагрузить компьютер, чтобы параметры автоматического входа в систему вступили в силу.", @@ -509,6 +518,7 @@ "RestartMessage": "Перезагрузите компьютер, чтобы запустить агент и чтобы параметры автоматического входа вступили в силу.", "ReStreamLogsToFilesError": "Одновременное использование --disableloguploads и --reStreamLogsToFiles невозможно!", "RetryCountLimitExceeded": "Максимально допустимое количество попыток равно{0}, но имело{1}. Количество повторных попыток будет уменьшено до {0}.", + "RetryingReplaceAgent": "Попытка заменить агента (попытка {0} из {1}). Подождите {2} с перед следующей попыткой...", "RMApiFailure": "Сбой {0} API с кодом ошибки: {1}", "RMArtifactContainerDetailsInvalidError": "Артефакт не содержит допустимые сведения о контейнере: {0}", "RMArtifactContainerDetailsNotFoundError": "Артефакт не содержит сведений о контейнере: {0}", @@ -681,7 +691,8 @@ "UnrecognizedCmdArgs": "Нераспознанные входные аргументы командной строки: \"{0}\". Сведения об использовании: .\\config.cmd --help или ./config.sh --help", "UnregisteringAgent": "Удаление агента с сервера", "UnsupportedGitLfsVersion": "Агент не поддерживает текущую версию Git LFS \"{0}\". Выполните обновление до версии не ниже \"{1}\". Дополнительные сведения: https://github.com/git-lfs/git-lfs/issues/3571.", - "UnsupportedOsVersionByNet8": "Версия операционной системы, на которой работает этот агент ({0}), не поддерживается в предстоящем обновлении Pipelines Agent. Поддерживаемые версии операционных систем см. на сайте https://aka.ms/azdo-pipeline-agent-net8.", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "Обновление номера сборки", "UpdateBuildNumberForBuild": "Обновите номер сборки до {0} для сборки {1}", "UpdateInProgress": "Выполняется обновление агента, не завершайте его работу.", diff --git a/src/Misc/layoutbin/zh-CN/strings.json b/src/Misc/layoutbin/zh-CN/strings.json index 947291ce05..db6841fea5 100644 --- a/src/Misc/layoutbin/zh-CN/strings.json +++ b/src/Misc/layoutbin/zh-CN/strings.json @@ -10,6 +10,7 @@ "AddEnvironmentVMResourceTags": "环境虚拟机资源标记? (是/否)", "AgentAddedSuccessfully": "已成功添加代理。", "AgentAlreadyInsideContainer": "代理已在容器内运行时,不支持容器功能。请参考文档(https://go.microsoft.com/fwlink/?linkid=875268)", + "AgentCdnAccessFailWarning": "所需操作: Azure Pipelines 代理无法访问新的 CDN URL。立即将 'download.agent.dev.azure.com' 加入允许列表,以防止管道故障。详细信息: https://devblogs.microsoft.com/devops/cdn-domain-url-change-for-agents-in-pipelines/", "AgentDoesNotSupportContainerFeatureRhel6": "代理不支持 Red Hat Enterprise Linux 6 或 CentOS 6 上的容器功能。", "AgentDowngrade": "正在将代理降级到较低的版本。这通常是由于当前为 bug 修复而发布的代理回退所导致的。若要禁用此行为,请在启动代理之前设置环境变量 AZP_AGENT_DOWNGRADE_DISABLED=true。", "AgentExit": "代理将很快退出以进行更新,应该会在 10 秒内重新联机。", @@ -107,6 +108,7 @@ " 协商 (Kerberos 或 NTLM)", " alt (基本身份验证)", " 集成(Windows 默认凭据)", + "sp (服务主体)", "--token 与 --auth pat 一起使用。个人访问令牌。", " --userName 与 --auth negotiate 或 --auth alt 一起使用。指定 Windows 用户", " 名称格式: domain\\userName 或 userName@domain.com", @@ -275,6 +277,7 @@ "DirExpireLimit": "目录过期限制: {0} 天。", "DiscoverBuildDir": "发现超过 {0} 天未使用的过时生成目录。", "DiscoverReleaseDir": "发现超过 {{0}} 天未使用的过时发布目录。", + "DockerCommandFinalExitCode": "{0} 的最终退出代码: {1}", "DownloadAgent": "正在下载 {0} 代理", "DownloadArtifactFinished": "下载项目已完成。", "DownloadArtifacts": "下载项目", @@ -437,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "需要管理员特权才能取消配置作为 Windows 服务运行的代理。", "NetworkServiceNotFound": "找不到网络服务帐户", "NoArtifactsFound": "版本 \"{0}\" 中没有可用的项目。", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "任务需要的 {0} 已达到生命周期结束阶段。此操作被组织策略阻止。请将任务升级到 Node20 或 Node24。要暂时禁用此检查: 请设置 AGENT_RESTRICT_EOL_NODE_VERSIONS=false", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "{0} 操作系统不支持 {1}。请转而使用 {2}。请升级操作系统,以与未来更新保持兼容。", + "NodeVersionNotAvailable": "没有兼容的 Node.js 版本可用于主机执行。处理程序类型: {0}。如果所有可用版本都被 EOL 策略阻止,可能会出现此情况。请更新管道以使用 Node20 或 Node24 任务。要暂时禁用 EOL 策略,请设置 AGENT_RESTRICT_EOL_NODE_VERSIONS=false", "NoFolderToClean": "未找到指定的清理文件夹。没有要清理的文件", "NoRestart": "稍后重启计算机? (是/否)", "NoRestartSuggestion": "代理配置过程中启用了自动登录。建议重新启动计算机,使自动登录设置生效。", @@ -509,6 +518,7 @@ "RestartMessage": "重启计算机以启动代理,并使自动登录设置生效。", "ReStreamLogsToFilesError": "不能同时使用 --disableloguploads 和 --reStreamLogsToFiles!", "RetryCountLimitExceeded": "允许的最大尝试次数为 {0} 但结果为 {1}。重试尝试计数将减少到 {0}。", + "RetryingReplaceAgent": "正在重试替换代理(第 {0} 次尝试/共 {1} 次)。下次尝试前等待 {2} 秒...", "RMApiFailure": "API {0} 失败,错误代码 {1}", "RMArtifactContainerDetailsInvalidError": "项目没有有效的容器详细信息: {0}", "RMArtifactContainerDetailsNotFoundError": "项目不包含容器详细信息: {0}", @@ -681,7 +691,8 @@ "UnrecognizedCmdArgs": "无法识别的命令行输入参数:“{0}”。有关用法,请参阅:.\\config.cmd --help 或 ./config.sh --help", "UnregisteringAgent": "正在从服务器中删除代理", "UnsupportedGitLfsVersion": "当前的 Git LFS 版本为“{0}”,但代理不支持该版本。请至少升级到版本“{1}”。有关更多详细信息,请参阅 https://github.com/git-lfs/git-lfs/issues/3571。", - "UnsupportedOsVersionByNet8": "管道代理即将更新的版本不支持此代理目前在其中运行的操作系统版本 ({0})。有关支持的操作系统版本,请参阅 https://aka.ms/azdo-pipeline-agent-net8。", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "更新生成号", "UpdateBuildNumberForBuild": "为生成将内部版本号更新为 {0} 以生成 {1}", "UpdateInProgress": "代理更新正在进行中,请勿关闭代理。", diff --git a/src/Misc/layoutbin/zh-TW/strings.json b/src/Misc/layoutbin/zh-TW/strings.json index 845803df5c..3489868ad8 100644 --- a/src/Misc/layoutbin/zh-TW/strings.json +++ b/src/Misc/layoutbin/zh-TW/strings.json @@ -10,6 +10,7 @@ "AddEnvironmentVMResourceTags": "環境虛擬機器資源標籤? (是/否)", "AgentAddedSuccessfully": "已成功新增代理程式", "AgentAlreadyInsideContainer": "當代理程式已在容器中執行時,不支援容器功能。請參閱文件 (https://go.microsoft.com/fwlink/?linkid=875268)", + "AgentCdnAccessFailWarning": "需要動作:Azure Pipelines 代理程式無法觸達新的 CDN URL。允許清單 'download.agent.dev.azure.com' 現在可防止管線失敗。詳細資料:https://devblogs.microsoft.com/devops/cdn-domain-url-change-for-agents-in-pipelines/", "AgentDoesNotSupportContainerFeatureRhel6": "代理程式不支援 Red Hat Enterprise Linux 6 或 CentOS 6 上的容器功能。", "AgentDowngrade": "將代理程式降級至較舊的版本。這通常是因為目前發佈的代理程式進行復原,以修正錯誤。若要停用此行為,請在啟動代理程式之前,設定環境變數 AZP_AGENT_DOWNGRADE_DISABLED=true。", "AgentExit": "代理程式即將結束以進行更新,應會在 10 秒內重新上線。", @@ -107,9 +108,10 @@ " 交涉 (Kerberos 或 NTLM)", " alt (基本驗證)", " 整合式 (Windows 預設認證)", + "sp (服務主體)", " --token 與 --auth pat. 搭配使用。個人存取權杖。", " --userName 與 --auth 協商或 --auth alt 搭配使用。指定 Windows 使用者", - " 名稱的格式: domain\\userName or userName@domain.com", + " 名稱的格式: domain\\userName 或 userName@domain.com", " --password 與 --auth 協商或 --auth alt 搭配使用。", " --unattended 自動設定。將不會提示您。所有答案都必須", " 提供給命令列。", @@ -275,6 +277,7 @@ "DirExpireLimit": "目錄到期限制: {0} 天。", "DiscoverBuildDir": "探索超過 {0} 天未曾使用的過時組建目錄。", "DiscoverReleaseDir": "探索超過 {0} 天未曾使用的過時版本目錄。", + "DockerCommandFinalExitCode": "{0} 的最終結束代碼: {1}", "DownloadAgent": "正在下載 {0} 代理程式", "DownloadArtifactFinished": "成品已下載完成。", "DownloadArtifacts": "下載成品", @@ -437,6 +440,12 @@ "NeedAdminForUnconfigWinServiceAgent": "需要系統管理員權限,才能取消以 Windows 服務執行之代理程式。", "NetworkServiceNotFound": "找不到網路服務帳戶", "NoArtifactsFound": "版本 '{0}' 中沒有可用的成品。", + "NodeEOLFallbackBlocked": "Would fallback to {0} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "工作需要的 {0} 已達終止服務。此受組織原則封鎖。請將工作升級至 Node20 或 Node24。若要暫時停用此檢查,請設定 AGENT_RESTRICT_EOL_NODE_VERSIONS=false", + "NodeEOLRetirementWarning": "Task {0} relies on an unsupported version of Node.js. Unsupported Node versions (6, 10, and 16) are being retired, and any pipelines using tasks dependent on these versions will begin to fail. Learn More https://aka.ms/node-runner-guidance", + "NodeEOLUpgradeWarning": "Task {0} relies on an unsupported version of Node.js. To avoid use of unsupported Node.js the task is being run on the latest available version of Node.js. The task may fail or work unexpectedly. Learn More https://aka.ms/node-runner-guidance", + "NodeGlibcFallbackWarning": "{0} 作業系統不支援 {1}。正在改用 {2}。請升級作業系統,以確保與未來更新的相容性。", + "NodeVersionNotAvailable": "無法提供相容的 Node.js 版本以供主機執行。處理常式類型: {0}。若所有可用版本均遭到終止服務原則封鎖,可能會發生此情況。請更新您的管線以使用 Node20 或 Node24 工作。若要暫時停用終止服務原則,請設定 AGENT_RESTRICT_EOL_NODE_VERSIONS=false", "NoFolderToClean": "找不到指定的清除資料夾。沒有可清除的項目", "NoRestart": "要稍後重新啟動電腦嗎? (是/否)", "NoRestartSuggestion": "自動登入已在代理程式組態期間啟用。建議您重新啟動電腦,自動登入設定才會生效。", @@ -509,6 +518,7 @@ "RestartMessage": "重新啟動電腦以啟動代理程式,並讓自動登入設定生效。", "ReStreamLogsToFilesError": "您不能同時使用 --disableloguploads 和 --reStreamLogsToFiles!", "RetryCountLimitExceeded": "允許的嘗試次數上限為 {0} 但結果為 {1}。請重試將嘗試次數將減少為 {0}。", + "RetryingReplaceAgent": "重試取代代理程式 (嘗試 {0} / {1})。在下次嘗試前等待 {2} 秒...", "RMApiFailure": "Api {0} 失敗,錯誤碼 {1}", "RMArtifactContainerDetailsInvalidError": "成品沒有有效的容器詳細資料: {0}", "RMArtifactContainerDetailsNotFoundError": "成品未包含容器詳細資料: {0}", @@ -681,7 +691,8 @@ "UnrecognizedCmdArgs": "無法辨識的命令列輸入引數: '{0}'。如需使用方式,請參閱: .\\config.cmd --help 或 ./config.sh --help", "UnregisteringAgent": "正在從伺服器移除代理程式", "UnsupportedGitLfsVersion": "您目前的 Git LFS 版本為 '{0}',代理程式不支援此版本。請升級至 '{1}' 以上的版本。如需詳細資料,請參閱 https://github.com/git-lfs/git-lfs/issues/3571。", - "UnsupportedOsVersionByNet8": "管線代理程式即將更新時,不支援此代理程式在 ({0}) 上執行的操作系統版本。如需支援的操作系統版本,請參閱 https://aka.ms/azdo-pipeline-agent-net8.", + "UnsupportedOsVersionByNet10": "The operating system version this agent is running on ({0}) is not supported per Net10 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net10.", + "UnsupportedOsVersionByNet8": "The operating system version this agent is running on ({0}) is not supported per Net8 requirements and will not be supported in an upcoming update to the Pipelines Agent. For supported operating system versions, see https://aka.ms/azdo-pipeline-agent-net8.", "UpdateBuildNumber": "更新組建編號", "UpdateBuildNumberForBuild": "將組建 {1} 的組建編號更新為 {0}", "UpdateInProgress": "代理程式更新進行中,請勿關閉代理程式。", diff --git a/src/Test/CodeCoverage.runsettings b/src/Test/CodeCoverage.runsettings index 7bcacec67c..1eb4d49363 100644 --- a/src/Test/CodeCoverage.runsettings +++ b/src/Test/CodeCoverage.runsettings @@ -9,6 +9,7 @@ .*buildxl.* + .*System.* .*ncrontab.* .*runtimecontracts\.dll .*test\.dll diff --git a/src/Test/L0/Listener/AgentL0.cs b/src/Test/L0/Listener/AgentL0.cs index 87e8f5837a..ffb9862008 100644 --- a/src/Test/L0/Listener/AgentL0.cs +++ b/src/Test/L0/Listener/AgentL0.cs @@ -73,7 +73,7 @@ private JobCancelMessage CreateJobCancelMessage() [Trait("Category", "Agent")] //process 2 new job messages, and one cancel message [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope")] - public async void TestRunAsync() + public async Task TestRunAsync() { using (var hc = new TestHostContext(this)) using (var agent = new Agent.Listener.Agent()) @@ -191,7 +191,7 @@ public async void TestRunAsync() [MemberData("RunAsServiceTestData")] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestExecuteCommandForRunAsService(string[] args, bool configureAsService, Times expectedTimes) + public async Task TestExecuteCommandForRunAsService(string[] args, bool configureAsService, Times expectedTimes) { using (var hc = new TestHostContext(this)) using (var agent = new Agent.Listener.Agent()) @@ -226,7 +226,7 @@ public async void TestExecuteCommandForRunAsService(string[] args, bool configur [Trait("Level", "L0")] [Trait("Category", "Agent")] //process 2 new job messages, and one cancel message - public async void TestMachineProvisionerCLI() + public async Task TestMachineProvisionerCLI() { using (var hc = new TestHostContext(this)) using (var agent = new Agent.Listener.Agent()) @@ -264,7 +264,7 @@ public async void TestMachineProvisionerCLI() [Trait("Level", "L0")] [Trait("Category", "Agent")] //process 2 new job messages, and one cancel message - public async void TestMachineProvisionerCLICompat() + public async Task TestMachineProvisionerCLICompat() { using (var hc = new TestHostContext(this)) using (var agent = new Agent.Listener.Agent()) @@ -301,7 +301,7 @@ public async void TestMachineProvisionerCLICompat() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestRunOnce() + public async Task TestRunOnce() { using (var hc = new TestHostContext(this)) using (var agent = new Agent.Listener.Agent()) @@ -400,7 +400,7 @@ public async void TestRunOnce() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestRunOnceOnlyTakeOneJobMessage() + public async Task TestRunOnceOnlyTakeOneJobMessage() { using (var hc = new TestHostContext(this)) using (var agent = new Agent.Listener.Agent()) @@ -506,7 +506,7 @@ public async void TestRunOnceOnlyTakeOneJobMessage() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestRunOnceHandleUpdateMessage() + public async Task TestRunOnceHandleUpdateMessage() { using (var hc = new TestHostContext(this)) using (var agent = new Agent.Listener.Agent()) @@ -604,7 +604,7 @@ public async void TestRunOnceHandleUpdateMessage() [InlineData("--version")] [InlineData("--commit")] [InlineData("--bad-argument", Constants.Agent.ReturnCode.TerminatedError)] - public async void TestInfoArgumentsCLI(string arg, int expected = Constants.Agent.ReturnCode.Success) + public async Task TestInfoArgumentsCLI(string arg, int expected = Constants.Agent.ReturnCode.Success) { using (var hc = new TestHostContext(this)) { @@ -638,7 +638,7 @@ public async void TestInfoArgumentsCLI(string arg, int expected = Constants.Agen [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestExitsIfUnconfigured() + public async Task TestExitsIfUnconfigured() { using (var hc = new TestHostContext(this)) { @@ -675,7 +675,7 @@ public async void TestExitsIfUnconfigured() [InlineData("configure", true)] //TODO: this passes. If already configured, probably should error out asked to configure again [InlineData("remove", false)] //TODO: this passes. If already not configured, probably should error out [InlineData("remove", true)] - public async void TestConfigureCLI(string arg, bool IsConfigured, int expected = Constants.Agent.ReturnCode.Success) + public async Task TestConfigureCLI(string arg, bool IsConfigured, int expected = Constants.Agent.ReturnCode.Success) { using (var hc = new TestHostContext(this)) { @@ -728,7 +728,7 @@ public async void TestConfigureCLI(string arg, bool IsConfigured, int expected = [Trait("Category", "Agent")] //process 1 job message and one metadata message [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope")] - public async void TestMetadataUpdate() + public async Task TestMetadataUpdate() { using (var hc = new TestHostContext(this)) using (var agent = new Agent.Listener.Agent()) diff --git a/src/Test/L0/Listener/Configuration/AgentAutoLogonTestL0.cs b/src/Test/L0/Listener/Configuration/AgentAutoLogonTestL0.cs index 9713316a65..bd57209f5e 100644 --- a/src/Test/L0/Listener/Configuration/AgentAutoLogonTestL0.cs +++ b/src/Test/L0/Listener/Configuration/AgentAutoLogonTestL0.cs @@ -39,7 +39,7 @@ public sealed class AgentAutoLogonTestL0 [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestAutoLogonConfiguration() + public async Task TestAutoLogonConfiguration() { using (var hc = new TestHostContext(this)) { @@ -60,7 +60,7 @@ public async void TestAutoLogonConfiguration() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestAutoLogonRunOnce() + public async Task TestAutoLogonRunOnce() { using (var hc = new TestHostContext(this)) { @@ -85,7 +85,7 @@ public async void TestAutoLogonRunOnce() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestAutoLogonConfigurationForDotAsDomainName() + public async Task TestAutoLogonConfigurationForDotAsDomainName() { using (var hc = new TestHostContext(this)) { @@ -110,7 +110,7 @@ public async void TestAutoLogonConfigurationForDotAsDomainName() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestAutoLogonConfigurationForDifferentUser() + public async Task TestAutoLogonConfigurationForDifferentUser() { using (var hc = new TestHostContext(this)) { @@ -130,7 +130,7 @@ public async void TestAutoLogonConfigurationForDifferentUser() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestAutoLogonUnConfigure() + public async Task TestAutoLogonUnConfigure() { //strategy- //1. fill some existing values in the registry @@ -158,7 +158,7 @@ public async void TestAutoLogonUnConfigure() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestAutoLogonUnConfigureForDifferentUser() + public async Task TestAutoLogonUnConfigureForDifferentUser() { //strategy- //1. fill some existing values in the registry diff --git a/src/Test/L0/Listener/Configuration/AgentCapabilitiesProviderTestL0.cs b/src/Test/L0/Listener/Configuration/AgentCapabilitiesProviderTestL0.cs index c5e5e9e902..41cdfae000 100644 --- a/src/Test/L0/Listener/Configuration/AgentCapabilitiesProviderTestL0.cs +++ b/src/Test/L0/Listener/Configuration/AgentCapabilitiesProviderTestL0.cs @@ -18,7 +18,7 @@ public sealed class AgentCapabilitiesProviderTestL0 [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestGetCapabilities() + public async Task TestGetCapabilities() { using (var hc = new TestHostContext(this)) using (var tokenSource = new CancellationTokenSource()) @@ -45,7 +45,7 @@ public async void TestGetCapabilities() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestInteractiveSessionCapability() + public async Task TestInteractiveSessionCapability() { using (var hc = new TestHostContext(this)) using (var tokenSource = new CancellationTokenSource()) diff --git a/src/Test/L0/Listener/Configuration/UserCapabilitiesProviderTestL0.cs b/src/Test/L0/Listener/Configuration/UserCapabilitiesProviderTestL0.cs index 4971d451db..e42ef64703 100644 --- a/src/Test/L0/Listener/Configuration/UserCapabilitiesProviderTestL0.cs +++ b/src/Test/L0/Listener/Configuration/UserCapabilitiesProviderTestL0.cs @@ -16,7 +16,7 @@ public sealed class UserCapabilitiesProviderTestL0 [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestGetCapabilitiesWithDotCapabilities() + public async Task TestGetCapabilitiesWithDotCapabilities() { using (var hc = new TestHostContext(this)) using (var tokenSource = new CancellationTokenSource()) @@ -52,7 +52,7 @@ public async void TestGetCapabilitiesWithDotCapabilities() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void TestGetCapabilitiesWithoutDotCapabilities() + public async Task TestGetCapabilitiesWithoutDotCapabilities() { using (var hc = new TestHostContext(this)) using (var tokenSource = new CancellationTokenSource()) diff --git a/src/Test/L0/Listener/JobDispatcherL0.cs b/src/Test/L0/Listener/JobDispatcherL0.cs index bd716381fd..07966addc0 100644 --- a/src/Test/L0/Listener/JobDispatcherL0.cs +++ b/src/Test/L0/Listener/JobDispatcherL0.cs @@ -48,7 +48,7 @@ private Pipelines.AgentJobRequestMessage CreateJobRequestMessage() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void DispatchesJobRequest() + public async Task DispatchesJobRequest() { //Arrange using (var hc = new TestHostContext(this)) @@ -98,7 +98,7 @@ public async void DispatchesJobRequest() [Trait("Level", "L0")] [Trait("Category", "Agent")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope")] - public async void DispatcherRenewJobRequest() + public async Task DispatcherRenewJobRequest() { //Arrange using (var hc = new TestHostContext(this)) @@ -157,7 +157,7 @@ public async void DispatcherRenewJobRequest() [Trait("Level", "L0")] [Trait("Category", "Agent")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope")] - public async void DispatcherRenewJobRequestStopOnJobNotFoundExceptions() + public async Task DispatcherRenewJobRequestStopOnJobNotFoundExceptions() { //Arrange using (var hc = new TestHostContext(this)) @@ -217,7 +217,7 @@ public async void DispatcherRenewJobRequestStopOnJobNotFoundExceptions() [Trait("Level", "L0")] [Trait("Category", "Agent")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope")] - public async void DispatcherRenewJobRequestStopOnJobTokenExpiredExceptions() + public async Task DispatcherRenewJobRequestStopOnJobTokenExpiredExceptions() { //Arrange using (var hc = new TestHostContext(this)) @@ -277,7 +277,7 @@ public async void DispatcherRenewJobRequestStopOnJobTokenExpiredExceptions() [Trait("Level", "L0")] [Trait("Category", "Agent")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope")] - public async void DispatcherRenewJobRequestRecoverFromExceptions() + public async Task DispatcherRenewJobRequestRecoverFromExceptions() { //Arrange using (var hc = new TestHostContext(this)) @@ -330,8 +330,8 @@ public async void DispatcherRenewJobRequestRecoverFromExceptions() Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed."); Assert.True(cancellationTokenSource.IsCancellationRequested); _agentServer.Verify(x => x.RenewAgentRequestAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(8)); - _agentServer.Verify(x => x.RefreshConnectionAsync(AgentConnectionType.JobRequest, It.IsAny()), Times.Exactly(3)); - _agentServer.Verify(x => x.SetConnectionTimeout(AgentConnectionType.JobRequest, It.IsAny()), Times.Once); + _agentServer.Verify(x => x.RefreshConnectionAsync(AgentConnectionType.JobRequest, It.IsAny()), Times.Exactly(3)); + _agentServer.Verify(x => x.ResetConnectionTimeout(AgentConnectionType.JobRequest, It.IsAny()), Times.Once); } } @@ -339,7 +339,7 @@ public async void DispatcherRenewJobRequestRecoverFromExceptions() [Trait("Level", "L0")] [Trait("Category", "Agent")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope")] - public async void DispatcherRenewJobRequestFirstRenewRetrySixTimes() + public async Task DispatcherRenewJobRequestFirstRenewRetrySixTimes() { //Arrange using (var hc = new TestHostContext(this)) @@ -395,7 +395,7 @@ public async void DispatcherRenewJobRequestFirstRenewRetrySixTimes() [Trait("Level", "L0")] [Trait("Category", "Agent")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA2000:Dispose objects before losing scope")] - public async void DispatcherRenewJobRequestStopOnExpiredRequest() + public async Task DispatcherRenewJobRequestStopOnExpiredRequest() { //Arrange using (var hc = new TestHostContext(this)) @@ -453,15 +453,15 @@ public async void DispatcherRenewJobRequestStopOnExpiredRequest() Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed."); Assert.False(cancellationTokenSource.IsCancellationRequested); _agentServer.Verify(x => x.RenewAgentRequestAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(5)); - _agentServer.Verify(x => x.RefreshConnectionAsync(AgentConnectionType.JobRequest, It.IsAny()), Times.Exactly(3)); - _agentServer.Verify(x => x.SetConnectionTimeout(AgentConnectionType.JobRequest, It.IsAny()), Times.Never); + _agentServer.Verify(x => x.RefreshConnectionAsync(AgentConnectionType.JobRequest, It.IsAny()), Times.Exactly(3)); + _agentServer.Verify(x => x.ResetConnectionTimeout(AgentConnectionType.JobRequest, It.IsAny()), Times.Never); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void DispatchesOneTimeJobRequest() + public async Task DispatchesOneTimeJobRequest() { //Arrange using (var hc = new TestHostContext(this)) diff --git a/src/Test/L0/Listener/MessageListenerL0.cs b/src/Test/L0/Listener/MessageListenerL0.cs index 8c4079e912..f3a23eebf2 100644 --- a/src/Test/L0/Listener/MessageListenerL0.cs +++ b/src/Test/L0/Listener/MessageListenerL0.cs @@ -64,7 +64,7 @@ private TestHostContext CreateTestContext([CallerMemberName] String testName = " [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void CreatesSession() + public async Task CreatesSession() { using (TestHostContext tc = CreateTestContext()) using (var tokenSource = new CancellationTokenSource()) @@ -104,7 +104,7 @@ public async void CreatesSession() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void DeleteSession() + public async Task DeleteSession() { using (TestHostContext tc = CreateTestContext()) using (var tokenSource = new CancellationTokenSource()) @@ -151,7 +151,7 @@ public async void DeleteSession() [Fact] [Trait("Level", "L0")] [Trait("Category", "Agent")] - public async void GetNextMessage() + public async Task GetNextMessage() { using (TestHostContext tc = CreateTestContext()) using (var tokenSource = new CancellationTokenSource()) @@ -229,6 +229,442 @@ public async void GetNextMessage() } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Agent")] + public async Task CreateSessionUsesExponentialBackoffWhenFlagEnabled() + { + using (TestHostContext tc = CreateTestContext()) + using (var tokenSource = new CancellationTokenSource()) + { + Tracing trace = tc.GetTrace(); + + try + { + int callCount = 0; + + _agentServer + .Setup(x => x.CreateAgentSessionAsync(_settings.PoolId, It.Is(y => y != null), tokenSource.Token)) + .Returns(() => + { + callCount++; + // Fail first 5 attempts to check delay at attempt 5 + if (callCount <= 5) + { + throw new Exception("Temporary failure"); + } + return Task.FromResult(new TaskAgentSession()); + }); + + _capabilitiesManager.Setup(x => x.GetCapabilitiesAsync(_settings, It.IsAny())).Returns(Task.FromResult(new Dictionary())); + _credMgr.Setup(x => x.LoadCredentials()).Returns(new Common.VssCredentials()); + + // Act + MessageListener listener = new MessageListener(); + listener.Initialize(tc); + + // Arrange - Set environment variable (simulating Agent.cs setting it after fetching FF) + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", "true"); + bool result = await listener.CreateSessionAsync(tokenSource.Token); + trace.Info($"result: {result}"); + + // Assert + Assert.True(result); + Assert.True(tc.CapturedDelays.Count >= 5, $"Should have at least 5 delays, got {tc.CapturedDelays.Count}"); + + // Check the 5th delay (index 4) + var delayAtAttempt5 = tc.CapturedDelays[4].TotalSeconds; + trace.Info($"Delay at attempt 5: {delayAtAttempt5:F1}s (expected >30s for exponential backoff)"); + + // Exponential should be > 30s (constant is 30s) + Assert.True(delayAtAttempt5 > 30, + $"Expected exponential (>30s), got {delayAtAttempt5:F1}s. This means the FF codepath was not executed even though the FF is enabled."); + } + finally + { + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", null); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Agent")] + public async Task CreateSessionUsesConstantBackoffWhenFlagDisabled() + { + using (TestHostContext tc = CreateTestContext()) + using (var tokenSource = new CancellationTokenSource()) + { + Tracing trace = tc.GetTrace(); + + try + { + int callCount = 0; + + _agentServer + .Setup(x => x.CreateAgentSessionAsync(_settings.PoolId, It.Is(y => y != null), tokenSource.Token)) + .Returns(() => + { + callCount++; + // Fail first 5 attempts + if (callCount <= 5) + { + throw new Exception("Temporary failure"); + } + return Task.FromResult(new TaskAgentSession()); + }); + + _capabilitiesManager.Setup(x => x.GetCapabilitiesAsync(_settings, It.IsAny())).Returns(Task.FromResult(new Dictionary())); + _credMgr.Setup(x => x.LoadCredentials()).Returns(new Common.VssCredentials()); + + // Act + MessageListener listener = new MessageListener(); + listener.Initialize(tc); + // Arrange - Ensure environment variable is not set (simulating FF being off) + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", "false"); + bool result = await listener.CreateSessionAsync(tokenSource.Token); + + // Assert + Assert.True(result); + Assert.True(tc.CapturedDelays.Count >= 5, $"Should have at least 5 delays, got {tc.CapturedDelays.Count}"); + + // Check the 5th delay (index 4) + var delayAtAttempt5 = tc.CapturedDelays[4].TotalSeconds; + trace.Info($"Delay at attempt 5: {delayAtAttempt5:F1}s (expected ~30s for constant backoff)"); + + // Constant should be exactly 30s + Assert.True(delayAtAttempt5 >= 29 && delayAtAttempt5 <= 31, + $"Expected ~30s (constant), got {delayAtAttempt5:F1}s. This proves FF codepath was executed even though the FF is disabled."); + } + finally + { + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", null); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Agent")] + public async Task GetNextMessageUsesExponentialBackoffWhenFlagEnabled() + { + using (TestHostContext tc = CreateTestContext()) + using (var tokenSource = new CancellationTokenSource()) + { + Tracing trace = tc.GetTrace(); + + try + { + // Create session first + var session = new TaskAgentSession(); + PropertyInfo sessionIdProperty = session.GetType().GetProperty("SessionId", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Assert.NotNull(sessionIdProperty); + sessionIdProperty.SetValue(session, Guid.NewGuid()); + + _agentServer + .Setup(x => x.CreateAgentSessionAsync(_settings.PoolId, It.Is(y => y != null), tokenSource.Token)) + .Returns(Task.FromResult(session)); + + int callCount = 0; + + _agentServer + .Setup(x => x.GetAgentMessageAsync(_settings.PoolId, session.SessionId, It.IsAny(), tokenSource.Token)) + .Returns(() => + { + callCount++; + // Fail first 6 attempts to check delay at attempt 6 + if (callCount <= 6) + { + throw new Exception("Temporary failure"); + } + return Task.FromResult(new TaskAgentMessage + { + MessageId = 123, + MessageType = JobRequestMessageTypes.AgentJobRequest, + Body = "test" + }); + }); + + _capabilitiesManager.Setup(x => x.GetCapabilitiesAsync(_settings, It.IsAny())).Returns(Task.FromResult(new Dictionary())); + _credMgr.Setup(x => x.LoadCredentials()).Returns(new Common.VssCredentials()); + + // Act + MessageListener listener = new MessageListener(); + listener.Initialize(tc); + await listener.CreateSessionAsync(tokenSource.Token); + + // Clear delays from CreateSession - we only want GetNextMessage delays + tc.CapturedDelays.Clear(); + + // Arrange - Set environment variable (simulating Agent.cs setting it after fetching FF) + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", "true"); + TaskAgentMessage message = await listener.GetNextMessageAsync(tokenSource.Token); + + // Assert - Check captured delays + Assert.NotNull(message); + + Assert.True(tc.CapturedDelays.Count >= 12, $"Should have at least 12 delays (6 backoffs + 6 random), got {tc.CapturedDelays.Count}"); + + // Check the 6th delay (index 10) + var delayAtAttempt6 = tc.CapturedDelays[10].TotalSeconds; + trace.Info($"Delay at attempt 6: {delayAtAttempt6:F1}s (expected >60s for exponential backoff)"); + + // Exponential should be > 60s (random is [30,60]s) + Assert.True(delayAtAttempt6 > 60, + $"Expected exponential (>60s), got {delayAtAttempt6:F1}s. This means the FF codepath was not executed even though the FF is enabled."); + } + finally + { + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", null); + } + + + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Agent")] + public async Task GetNextMessageUsesRandomBackoffWhenFlagDisabled() + { + using (TestHostContext tc = CreateTestContext()) + using (var tokenSource = new CancellationTokenSource()) + { + Tracing trace = tc.GetTrace(); + + try + { + //create session first + var session = new TaskAgentSession(); + PropertyInfo sessionIdProperty = session.GetType().GetProperty("SessionId", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Assert.NotNull(sessionIdProperty); + sessionIdProperty.SetValue(session, Guid.NewGuid()); + + _agentServer + .Setup(x => x.CreateAgentSessionAsync(_settings.PoolId, It.Is(y => y != null), tokenSource.Token)) + .Returns(Task.FromResult(session)); + + int callCount = 0; + + _agentServer + .Setup(x => x.GetAgentMessageAsync(_settings.PoolId, session.SessionId, It.IsAny(), tokenSource.Token)) + .Returns(() => + { + callCount++; + // Fail first 6 attempts + if (callCount <= 6) + { + throw new Exception("Temporary failure"); + } + return Task.FromResult(new TaskAgentMessage + { + MessageId = 456, + MessageType = JobRequestMessageTypes.AgentJobRequest, + Body = "test" + }); + }); + + _capabilitiesManager.Setup(x => x.GetCapabilitiesAsync(_settings, It.IsAny())).Returns(Task.FromResult(new Dictionary())); + _credMgr.Setup(x => x.LoadCredentials()).Returns(new Common.VssCredentials()); + + // Act + MessageListener listener = new MessageListener(); + listener.Initialize(tc); + await listener.CreateSessionAsync(tokenSource.Token); + + // Clear delays from CreateSession - we only want GetNextMessage delays + tc.CapturedDelays.Clear(); + + // Arrange - Ensure environment variable is not set (simulating FF being off) + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", "false"); + TaskAgentMessage message = await listener.GetNextMessageAsync(tokenSource.Token); + + // Assert - Check captured delays + Assert.NotNull(message); + + Assert.True(tc.CapturedDelays.Count >= 12, $"Should have at least 12 delays (6 backoffs + 6 random), got {tc.CapturedDelays.Count}"); + + // Check the 6th delay (index 10) + var delayAtAttempt6 = tc.CapturedDelays[10].TotalSeconds; + trace.Info($"Delay at attempt 6: {delayAtAttempt6:F1}s (expected [30,60]s for random backoff)"); + + // Random should be in [30,60]s range + Assert.True(delayAtAttempt6 >= 30 && delayAtAttempt6 <= 60, + $"Expected [30,60]s (random), got {delayAtAttempt6:F1}s. This proves FF codepath was executed even though the FF is disabled."); + } + finally + { + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", null); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Agent")] + public async Task KeepAliveUsesExponentialBackoffWhenFlagEnabled() + { + using (TestHostContext tc = CreateTestContext()) + using (var tokenSource = new CancellationTokenSource()) + { + Tracing trace = tc.GetTrace(); + + try + { + // Create session first + var session = new TaskAgentSession(); + PropertyInfo sessionIdProperty = session.GetType().GetProperty("SessionId", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Assert.NotNull(sessionIdProperty); + sessionIdProperty.SetValue(session, Guid.NewGuid()); + + _agentServer + .Setup(x => x.CreateAgentSessionAsync(_settings.PoolId, It.Is(y => y != null), tokenSource.Token)) + .Returns(Task.FromResult(session)); + + _capabilitiesManager.Setup(x => x.GetCapabilitiesAsync(_settings, It.IsAny())).Returns(Task.FromResult(new Dictionary())); + _credMgr.Setup(x => x.LoadCredentials()).Returns(new Common.VssCredentials()); + + int callCount = 0; + + // Setup GetAgentMessageAsync to track KeepAlive calls + _agentServer + .Setup(x => x.GetAgentMessageAsync(_settings.PoolId, session.SessionId, null, tokenSource.Token)) + .Returns(() => + { + callCount++; + // Fail first 5 attempts to check delay at attempt 5 + if (callCount <= 5) + { + throw new Exception("KeepAlive failure"); + } + // Cancel after success to stop the infinite loop + tokenSource.Cancel(); + return Task.FromResult(null); + }); + + // Act + MessageListener listener = new MessageListener(); + listener.Initialize(tc); + await listener.CreateSessionAsync(tokenSource.Token); + + // Clear delays from CreateSession - we only want KeepAlive delays + tc.CapturedDelays.Clear(); + + // Arrange - Set environment variable (simulating Agent.cs setting it after fetching FF) + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", "true"); + // Start KeepAlive in a task and let it run until cancellation + var keepAliveTask = listener.KeepAlive(tokenSource.Token); + + try + { + await keepAliveTask; + } + catch (OperationCanceledException) + { + // Expected when token is cancelled + } + + // Assert - Check captured delays + Assert.True(tc.CapturedDelays.Count >= 5, $"Should have at least 5 delays, got {tc.CapturedDelays.Count}"); + + // Check the 5th delay (index 4) + var delayAtAttempt5 = tc.CapturedDelays[4].TotalSeconds; + trace.Info($"KeepAlive delay at attempt 5: {delayAtAttempt5:F1}s (expected >30s for exponential backoff)"); + + // Exponential should be > 30s (constant is 30s) + Assert.True(delayAtAttempt5 > 30, + $"Expected exponential (>30s), got {delayAtAttempt5:F1}s. This means the FF codepath was not executed even though the FF is enabled."); + } + finally + { + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", null); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Agent")] + public async Task KeepAliveUsesConstantBackoffWhenFlagDisabled() + { + using (TestHostContext tc = CreateTestContext()) + using (var tokenSource = new CancellationTokenSource()) + { + Tracing trace = tc.GetTrace(); + + try + { + var session = new TaskAgentSession(); + var sessionIdProperty = session.GetType().GetProperty("SessionId", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + sessionIdProperty.SetValue(session, Guid.NewGuid()); + + _agentServer + .Setup(x => x.CreateAgentSessionAsync(_settings.PoolId, It.Is(y => y != null), tokenSource.Token)) + .Returns(Task.FromResult(session)); + + _capabilitiesManager.Setup(x => x.GetCapabilitiesAsync(_settings, It.IsAny())).Returns(Task.FromResult(new Dictionary())); + _credMgr.Setup(x => x.LoadCredentials()).Returns(new Common.VssCredentials()); + + var callTimes = new List(); + int callCount = 0; + + // Setup GetAgentMessageAsync to track KeepAlive calls + _agentServer + .Setup(x => x.GetAgentMessageAsync(_settings.PoolId, session.SessionId, null, tokenSource.Token)) + .Returns(() => + { + callTimes.Add(DateTime.UtcNow); + callCount++; + // Fail first 5 attempts + if (callCount <= 5) + { + throw new Exception("KeepAlive failure"); + } + // Cancel after success to stop the infinite loop + tokenSource.Cancel(); + return Task.FromResult(null); + }); + + // Act + MessageListener listener = new MessageListener(); + listener.Initialize(tc); + await listener.CreateSessionAsync(tokenSource.Token); + + // Clear delays from CreateSession - we only want KeepAlive delays + tc.CapturedDelays.Clear(); + + // Arrange - Ensure environment variable is not set (simulating FF being off) + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", "false"); + // Start KeepAlive in a task and let it run until cancellation + var keepAliveTask = listener.KeepAlive(tokenSource.Token); + + try + { + await keepAliveTask; + } + catch (OperationCanceledException) + { + // Expected when token is cancelled + } + + // Assert - Check captured delays + Assert.True(tc.CapturedDelays.Count >= 5, $"Should have at least 5 delays, got {tc.CapturedDelays.Count}"); + + // Check the 5th delay (index 4) + var delayAtAttempt5 = tc.CapturedDelays[4].TotalSeconds; + trace.Info($"KeepAlive delay at attempt 5: {delayAtAttempt5:F1}s (expected ~30s for constant backoff)"); + + // Constant should be exactly 30s + Assert.True(delayAtAttempt5 >= 29 && delayAtAttempt5 <= 31, + $"Expected ~30s (constant), got {delayAtAttempt5:F1}s. This proves FF codepath was executed even though the FF is disabled."); + } + finally + { + Environment.SetEnvironmentVariable("AGENT_ENABLE_PROGRESSIVE_RETRY_BACKOFF", null); + } + } + } + public void Dispose() { rsa.Dispose(); diff --git a/src/Test/L0/NodeHandler.GlibcTest.cs b/src/Test/L0/NodeHandler.GlibcTest.cs new file mode 100644 index 0000000000..057c5a9f1c --- /dev/null +++ b/src/Test/L0/NodeHandler.GlibcTest.cs @@ -0,0 +1,340 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies; +using Moq; +using Xunit; +using Agent.Sdk; + +namespace Microsoft.VisualStudio.Services.Agent.Tests +{ + public class NodeHandlerGlibcTest : IDisposable + { + private bool disposed = false; + + private class TestableGlibcCompatibilityInfoProvider : GlibcCompatibilityInfoProvider + { + public TestableGlibcCompatibilityInfoProvider(IHostContext hostContext) + : base(hostContext) + { + } + + protected override bool IsLinuxPlatform() => true; + + protected override bool NodeBinaryExists(string nodePath) => true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + disposed = true; + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_Node24GlibcError_ReturnsCorrectStatus() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc); + + SetupNodeProcessInvocation(processInvokerMock, "node24", shouldHaveGlibcError: true); + SetupNodeProcessInvocation(processInvokerMock, "node20_1", shouldHaveGlibcError: false); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(executionContextMock.Object); + + Assert.True(result.Node24HasGlibcError); + Assert.False(result.Node20HasGlibcError); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_BothVersionsSuccess_ReturnsCorrectStatus() + { + ResetGlibcCompatibilityInfoProviderCache(); + using (var hc = new TestHostContext(this)) + { + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc); + + SetupNodeProcessInvocation(processInvokerMock, "node24", shouldHaveGlibcError: false); + SetupNodeProcessInvocation(processInvokerMock, "node20_1", shouldHaveGlibcError: false); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(executionContextMock.Object); + + Assert.False(result.Node24HasGlibcError); + Assert.False(result.Node20HasGlibcError); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_UseNode20InUnsupportedSystem_SkipsNode20Check() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var knobs = new Dictionary + { + ["AGENT_USE_NODE20_IN_UNSUPPORTED_SYSTEM"] = "true" + }; + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc, knobs); + + SetupNodeProcessInvocation(processInvokerMock, "node24", shouldHaveGlibcError: true); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(executionContextMock.Object); + + Assert.True(result.Node24HasGlibcError); + Assert.False(result.Node20HasGlibcError); + + VerifyProcessNotCalled(processInvokerMock, "node20_1"); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_UseNode24InUnsupportedSystem_SkipsNode24Check() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var knobs = new Dictionary + { + ["AGENT_USE_NODE24_IN_UNSUPPORTED_SYSTEM"] = "true" + }; + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc, knobs); + + SetupNodeProcessInvocation(processInvokerMock, "node20_1", shouldHaveGlibcError: true); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(executionContextMock.Object); + + Assert.False(result.Node24HasGlibcError); + Assert.True(result.Node20HasGlibcError); + + VerifyProcessNotCalled(processInvokerMock, "node24"); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_BothUnsupportedSystemKnobs_SkipsBothChecks() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var knobs = new Dictionary + { + ["AGENT_USE_NODE20_IN_UNSUPPORTED_SYSTEM"] = "true", + ["AGENT_USE_NODE24_IN_UNSUPPORTED_SYSTEM"] = "true" + }; + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc, knobs); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(executionContextMock.Object); + + Assert.False(result.Node24HasGlibcError); + Assert.False(result.Node20HasGlibcError); + VerifyNoProcessesCalled(processInvokerMock); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_StaticCaching_WorksCorrectly() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc); + + SetupNodeProcessInvocation(processInvokerMock, "node24", shouldHaveGlibcError: false); + SetupNodeProcessInvocation(processInvokerMock, "node20_1", shouldHaveGlibcError: false); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(hc); + var result1 = await glibcChecker.CheckGlibcCompatibilityAsync(executionContextMock.Object); + var result2 = await glibcChecker.CheckGlibcCompatibilityAsync(executionContextMock.Object); + + Assert.False(result1.Node24HasGlibcError); + Assert.False(result1.Node20HasGlibcError); + Assert.False(result2.Node24HasGlibcError); + Assert.False(result2.Node20HasGlibcError); + VerifyProcessCalledOnce(processInvokerMock, "node24"); + VerifyProcessCalledOnce(processInvokerMock, "node20_1"); + } + } + + #region Helper Methods + + /// + /// Sets up the common test environment with process invoker and execution context mocks. + /// + /// Test host context + /// Optional knob settings to configure + /// Tuple of (processInvokerMock, executionContextMock) + private (Mock, Mock) SetupTestEnvironment(TestHostContext hc, Dictionary knobs = null) + { + var processInvokerMock = new Mock(); + var executionContextMock = new Mock(); + + for (int i = 0; i < 10; i++) + { + hc.EnqueueInstance(processInvokerMock.Object); + } + + var variables = new Dictionary(); + if (knobs != null) + { + foreach (var knob in knobs) + { + variables[knob.Key] = new VariableValue(knob.Value); + } + } + + List warnings = new List(); + executionContextMock + .Setup(x => x.Variables) + .Returns(new Variables(hc, copy: variables, warnings: out warnings)); + + executionContextMock + .Setup(x => x.GetScopedEnvironment()) + .Returns(new SystemEnvironment()); + + executionContextMock + .Setup(x => x.GetVariableValueOrDefault(It.IsAny())) + .Returns((string variableName) => + { + if (variables.TryGetValue(variableName, out VariableValue value)) + { + return value.Value; + } + return Environment.GetEnvironmentVariable(variableName); + }); + + executionContextMock.Setup(x => x.EmitHostNode20FallbackTelemetry(It.IsAny())); + executionContextMock.Setup(x => x.EmitHostNode24FallbackTelemetry(It.IsAny())); + + return (processInvokerMock, executionContextMock); + } + + /// + /// Verifies that a specific node process was never called. + /// + private void VerifyProcessNotCalled(Mock processInvokerMock, string nodeFolder) + { + processInvokerMock.Verify(x => x.ExecuteAsync( + It.IsAny(), + It.Is(fileName => fileName.Contains(nodeFolder)), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + } + + /// + /// Verifies that no processes were called at all. + /// + private void VerifyNoProcessesCalled(Mock processInvokerMock) + { + processInvokerMock.Verify(x => x.ExecuteAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + } + + /// + /// Verifies that a specific node process was called exactly once. + /// + private void VerifyProcessCalledOnce(Mock processInvokerMock, string nodeFolder) + { + processInvokerMock.Verify(x => x.ExecuteAsync( + It.IsAny(), + It.Is(fileName => fileName.Contains(nodeFolder)), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once); + } + + private void SetupNodeProcessInvocation(Mock processInvokerMock, string nodeFolder, bool shouldHaveGlibcError) + { + string nodeExePath = Path.Combine("externals", nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + + processInvokerMock.Setup(x => x.ExecuteAsync( + It.IsAny(), + It.Is(fileName => fileName.Contains(nodeExePath)), + "-v", + It.IsAny>(), + false, + It.IsAny(), + It.IsAny())) + .Callback, bool, Encoding, CancellationToken>( + (wd, fn, args, env, reqZero, enc, ct) => + { + if (shouldHaveGlibcError) + { + processInvokerMock.Raise(x => x.ErrorDataReceived += null, + processInvokerMock.Object, + new ProcessDataReceivedEventArgs("node: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found")); + } + else + { + processInvokerMock.Raise(x => x.OutputDataReceived += null, + processInvokerMock.Object, + new ProcessDataReceivedEventArgs($"v{(nodeFolder.Contains("24") ? "24" : "20")}.0.0")); + } + }) + .ReturnsAsync(shouldHaveGlibcError ? 1 : 0); + } + + private void ResetGlibcCompatibilityInfoProviderCache() + { + var glibcType = typeof(GlibcCompatibilityInfoProvider); + var supportsNode20Field = glibcType.GetField("_supportsNode20", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + var supportsNode24Field = glibcType.GetField("_supportsNode24", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + supportsNode20Field?.SetValue(null, null); + supportsNode24Field?.SetValue(null, null); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Test/L0/NodeHandlerCollections.cs b/src/Test/L0/NodeHandlerCollections.cs new file mode 100644 index 0000000000..e89c0942d1 --- /dev/null +++ b/src/Test/L0/NodeHandlerCollections.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xunit; + +namespace Microsoft.VisualStudio.Services.Agent.Tests +{ + /// + /// Single collection for ALL NodeHandler tests (legacy and unified). + /// This ensures sequential execution to prevent environment variable conflicts. + /// + [CollectionDefinition("Unified NodeHandler Tests")] + public class UnifiedNodeHandlerTestFixture : ICollectionFixture + { + // This class is never instantiated, it's just a collection marker + } +} \ No newline at end of file diff --git a/src/Test/L0/NodeHandlerL0.AllSpecs.cs b/src/Test/L0/NodeHandlerL0.AllSpecs.cs new file mode 100644 index 0000000000..3cd40692b4 --- /dev/null +++ b/src/Test/L0/NodeHandlerL0.AllSpecs.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Linq; +using System.Runtime.InteropServices; +using Agent.Sdk; +using Xunit; + +namespace Microsoft.VisualStudio.Services.Agent.Tests +{ + /// + /// Unified test runner for ALL NodeHandler test specifications. + /// Executes every scenario defined in NodeHandlerTestSpecs.AllScenarios. + /// + [Trait("Level", "L0")] + [Trait("Category", "NodeHandler")] + [Collection("Unified NodeHandler Tests")] + public sealed class NodeHandlerL0AllSpecs : NodeHandlerTestBase + { + [Theory] + [MemberData(nameof(GetAllNodeHandlerScenarios))] + public void NodeHandler_AllScenarios_on_legacy(TestScenario scenario) + { + RunScenarioAndAssert(scenario, useStrategy: false); + } + + [Theory] + [MemberData(nameof(GetAllNodeHandlerScenarios))] + public void NodeHandler_AllScenarios_on_strategy(TestScenario scenario) + { + RunScenarioAndAssert(scenario, useStrategy: true); + } + + public static object[][] GetAllNodeHandlerScenarios() + { + var scenarios = NodeHandlerTestSpecs.AllScenarios.ToList(); + + // Skip container tests on macOS since they always use cross-platform logic + // This is expected behavior - macOS agent binaries cannot run in typical Linux containers + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + scenarios = scenarios.Where(s => !s.InContainer).ToList(); + } + + return scenarios + .Select(scenario => new object[] { scenario }) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Test/L0/NodeHandlerL0.TestSpecifications.cs b/src/Test/L0/NodeHandlerL0.TestSpecifications.cs new file mode 100644 index 0000000000..f2e03d962a --- /dev/null +++ b/src/Test/L0/NodeHandlerL0.TestSpecifications.cs @@ -0,0 +1,904 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Tests +{ + public static class NodeHandlerTestSpecs + { + public static readonly TestScenario[] AllScenarios = new[] + { + // ============================================ + // GROUP 0: CUSTOM NODE SCENARIOS + // ============================================ + new TestScenario( + name: "CustomNode_Host_OverridesHandlerData", + description: "Custom node path takes priority over handler data type", + handlerData: typeof(Node20_1HandlerData), + customNodePath: "/usr/local/custom/node", + inContainer: false, + expectedNode: "/usr/local/custom/node" + ), + + new TestScenario( + name: "CustomNode_Host_BypassesAllKnobs", + description: "Custom node path ignores all global node version knobs", + handlerData: typeof(Node10HandlerData), + knobs: new() + { + ["AGENT_USE_NODE24"] = "true", + ["AGENT_USE_NODE20_1"] = "true", + ["AGENT_USE_NODE10"] = "true" + }, + customNodePath: "/opt/my-node/bin/node", + inContainer: false, + expectedNode: "/opt/my-node/bin/node" + ), + + new TestScenario( + name: "CustomNode_Host_BypassesEOLPolicy", + description: "Custom node path bypasses EOL policy restrictions", + handlerData: typeof(Node10HandlerData), + knobs: new() + { + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" + }, + customNodePath: "/legacy/node6/bin/node", + inContainer: false, + expectedNode: "/legacy/node6/bin/node" + ), + + new TestScenario( + name: "CustomNode_HighestPriority_OverridesEverything", + description: "Custom path has highest priority - overrides all knobs, EOL policy, and glibc errors", + handlerData: typeof(Node10HandlerData), + knobs: new() + { + ["AGENT_USE_NODE24"] = "true", + ["AGENT_USE_NODE20_1"] = "true", + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true", + ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "false" + }, + node20GlibcError: true, + node24GlibcError: true, + customNodePath: "/ultimate/override/node", + inContainer: false, + expectedNode: "/ultimate/override/node" + ), + + new TestScenario( + name: "CustomNode_NullPath_FallsBackToNormalLogic", + description: "Null custom node path falls back to standard node selection", + handlerData: typeof(Node24HandlerData), + knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true" }, + customNodePath: null, + inContainer: false, + expectedNode: "node24" + ), + + new TestScenario( + name: "CustomNode_EmptyString_IgnoredFallsBackToNormalLogic", + description: "Empty custom node path is ignored, falls back to normal handler logic", + handlerData: typeof(Node20_1HandlerData), + customNodePath: "", + inContainer: false, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "CustomNode_WhitespaceOnly_IgnoredFallsBackToNormalLogic", + description: "Whitespace-only custom node path is ignored, falls back to normal handler logic", + handlerData: typeof(Node16HandlerData), + customNodePath: " ", + inContainer: false, + expectedNode: "node16" + ), + + // ======================================================================================== + // GROUP 1: NODE6 SCENARIOS (Node6HandlerData - EOL) + // ======================================================================================== + new TestScenario( + name: "Node6_DefaultBehavior", + description: "Node6 handler works when in default behavior (EOL policy disabled)", + handlerData: typeof(NodeHandlerData), + knobs: new() {}, + expectedNode: "node" + ), + + new TestScenario( + name: "Node6_DefaultBehavior_EOLPolicyDisabled", + description: "Node6 handler works when EOL policy is disabled", + handlerData: typeof(NodeHandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false" }, + expectedNode: "node" + ), + + new TestScenario( + name: "Node6_EOLPolicyEnabled_UpgradesToNode24", + description: "Node6 handler with EOL policy: legacy allows Node6, strategy-based upgrades to Node24", + handlerData: typeof(NodeHandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + legacyExpectedNode: "node", + strategyExpectedNode: "node24", + strategyExpectedWarning: "NodeEOLUpgradeWarning" + ), + + new TestScenario( + name: "Node6_WithGlobalUseNode10Knob", + description: "Node6 handler with global Node10 knob: legacy uses Node10, strategy-based ignores deprecated knob and uses Node6", + handlerData: typeof(NodeHandlerData), + knobs: new() { ["AGENT_USE_NODE10"] = "true" }, + legacyExpectedNode: "node10", + strategyExpectedNode: "node" + ), + + new TestScenario( + name: "Node6_WithGlobalUseNode20Knob", + description: "Global Node20 knob overrides Node6 handler data", + handlerData: typeof(NodeHandlerData), + knobs: new() { ["AGENT_USE_NODE20_1"] = "true" }, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Node6_WithGlobalUseNode24Knob", + description: "Global Node24 knob overrides Node6 handler data", + handlerData: typeof(NodeHandlerData), + knobs: new() { ["AGENT_USE_NODE24"] = "true" }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node6_PriorityTest_UseNode24OverridesUseNode20", + description: "Node24 global knob takes priority over Node20 global knob with Node6 handler", + handlerData: typeof(NodeHandlerData), + knobs: new() + { + ["AGENT_USE_NODE20_1"] = "true", + ["AGENT_USE_NODE24"] = "true" + }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node6_PriorityTest_UseNode20OverridesUseNode10", + description: "Node20 global knob takes priority over Node10 global knob with Node6 handler", + handlerData: typeof(NodeHandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "true", + ["AGENT_USE_NODE20_1"] = "true" + }, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Node6_MultipleKnobs_GlobalWins", + description: "Global Node24 knob takes highest priority when multiple knobs are set with Node6 handler", + handlerData: typeof(NodeHandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "true", + ["AGENT_USE_NODE20_1"] = "true", + ["AGENT_USE_NODE24"] = "true" + }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node6_AllGlobalKnobsDisabled_UsesHandler", + description: "Node6 handler uses handler data when all global knobs are disabled", + handlerData: typeof(NodeHandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "false", + ["AGENT_USE_NODE20_1"] = "false", + ["AGENT_USE_NODE24"] = "false" + }, + expectedNode: "node" + ), + + new TestScenario( + name: "Node6_EOLPolicy_Node24GlibcError_FallsBackToNode20", + description: "Node6 handler with EOL policy and Node24 glibc error: legacy allows Node6, strategy-based falls back to Node20", + handlerData: typeof(NodeHandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + node24GlibcError: true, + legacyExpectedNode: "node", + strategyExpectedNode: "node20_1", + strategyExpectedWarning: "NodeEOLUpgradeWarning" + ), + + new TestScenario( + name: "Node6_EOLPolicy_BothNode24AndNode20GlibcErrors_ThrowsError", + description: "Node6 handler with EOL policy and both newer versions having glibc errors: legacy allows Node6, strategy-based throws error", + handlerData: typeof(NodeHandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + node24GlibcError: true, + node20GlibcError: true, + legacyExpectedNode: "node", + expectedErrorType: typeof(NotSupportedException), + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: NodeHandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" + ), + + // ======================================================================================== + // GROUP 2: NODE10 SCENARIOS (Node10HandlerData - EOL) + // ======================================================================================== + + new TestScenario( + name: "Node10_DefaultBehavior", + description: "Node10 handler uses Node10", + handlerData: typeof(Node10HandlerData), + knobs: new() {}, + expectedNode: "node10" + ), + + new TestScenario( + name: "Node10_DefaultBehavior_EOLPolicyDisabled", + description: "Node10 handler uses Node10 when EOL policy is disabled", + handlerData: typeof(Node10HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false" }, + expectedNode: "node10" + ), + + new TestScenario( + name: "Node10_EOLPolicyEnabled_UpgradesToNode24", + description: "Node10 handler with EOL policy: legacy allows Node10, strategy-based upgrades to Node24", + handlerData: typeof(Node10HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + legacyExpectedNode: "node10", + strategyExpectedNode: "node24", + strategyExpectedWarning: "NodeEOLUpgradeWarning" + ), + + new TestScenario( + name: "Node10_WithGlobalUseNode10Knob", + description: "Global Node10 knob reinforces Node10 handler data", + handlerData: typeof(Node10HandlerData), + knobs: new() { ["AGENT_USE_NODE10"] = "true" }, + expectedNode: "node10" + ), + + new TestScenario( + name: "Node10_WithGlobalUseNode20Knob", + description: "Global Node20 knob overrides Node10 handler data", + handlerData: typeof(Node10HandlerData), + knobs: new() { ["AGENT_USE_NODE20_1"] = "true" }, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Node10_WithGlobalUseNode24Knob", + description: "Global Node24 knob overrides Node10 handler data", + handlerData: typeof(Node10HandlerData), + knobs: new() { ["AGENT_USE_NODE24"] = "true" }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node10_PriorityTest_UseNode24OverridesUseNode20", + description: "Node24 global knob takes priority over Node20 global knob with Node10 handler", + handlerData: typeof(Node10HandlerData), + knobs: new() + { + ["AGENT_USE_NODE20_1"] = "true", + ["AGENT_USE_NODE24"] = "true" + }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node10_PriorityTest_UseNode20OverridesUseNode10", + description: "Node20 global knob takes priority over Node10 global knob with Node10 handler", + handlerData: typeof(Node10HandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "true", + ["AGENT_USE_NODE20_1"] = "true" + }, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Node10_MultipleKnobs_GlobalWins", + description: "Global Node24 knob takes highest priority when multiple knobs are set with Node10 handler", + handlerData: typeof(Node10HandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "true", + ["AGENT_USE_NODE20_1"] = "true", + ["AGENT_USE_NODE24"] = "true" + }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node10_AllGlobalKnobsDisabled_UsesHandler", + description: "Node10 handler uses handler data when all global knobs are disabled", + handlerData: typeof(Node10HandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "false", + ["AGENT_USE_NODE20_1"] = "false", + ["AGENT_USE_NODE24"] = "false" + }, + expectedNode: "node10" + ), + + new TestScenario( + name: "Node10_EOLPolicy_Node24GlibcError_FallsBackToNode20", + description: "Node10 handler with EOL policy and Node24 glibc error: legacy allows Node10, strategy-based falls back to Node20", + handlerData: typeof(Node10HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + node24GlibcError: true, + legacyExpectedNode: "node10", + strategyExpectedNode: "node20_1", + strategyExpectedWarning: "NodeEOLUpgradeWarning" + ), + + new TestScenario( + name: "Node10_EOLPolicy_BothNode24AndNode20GlibcErrors_ThrowsError", + description: "Node10 handler with EOL policy and both newer versions having glibc errors: legacy allows Node10, strategy-based throws error", + handlerData: typeof(Node10HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + node24GlibcError: true, + node20GlibcError: true, + legacyExpectedNode: "node10", + expectedErrorType: typeof(NotSupportedException), + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node10HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" + ), + + // ======================================================================================== + // GROUP 3: NODE16 SCENARIOS (Node16HandlerData) + // ======================================================================================== + + new TestScenario( + name: "Node16_DefaultBehavior_EOLPolicyDisabled", + description: "Node16 handler uses Node16 when EOL policy is disabled", + handlerData: typeof(Node16HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false" }, + expectedNode: "node16" + ), + + new TestScenario( + name: "Node16_DefaultEOLPolicy_AllowsNode16", + description: "Node16 handler uses Node16 when EOL policy is default (disabled)", + handlerData: typeof(Node16HandlerData), + knobs: new() { }, + expectedNode: "node16" + ), + + new TestScenario( + name: "Node16_EOLPolicyEnabled_UpgradesToNode24", + description: "Node16 handler with EOL policy: legacy allows Node16, strategy-based upgrades to Node24", + handlerData: typeof(Node16HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + legacyExpectedNode: "node16", + strategyExpectedNode: "node24", + strategyExpectedWarning: "NodeEOLUpgradeWarning" + ), + + new TestScenario( + name: "Node16_EOLPolicy_Node24GlibcError_FallsBackToNode20", + description: "Node16 handler with EOL policy and Node24 glibc error: legacy allows Node16, strategy-based falls back to Node20", + handlerData: typeof(Node16HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + node24GlibcError: true, + legacyExpectedNode: "node16", + strategyExpectedNode: "node20_1", + strategyExpectedWarning: "NodeEOLUpgradeWarning" + ), + + new TestScenario( + name: "Node16_EOLPolicy_BothNode24AndNode20GlibcErrors_ThrowsError", + description: "Node16 handler with EOL policy and both newer versions having glibc errors: legacy allows Node16, strategy-based throws error", + handlerData: typeof(Node16HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + node24GlibcError: true, + node20GlibcError: true, + legacyExpectedNode: "node16", + expectedErrorType: typeof(NotSupportedException), + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node16HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" + ), + + // ======================================================================================== + // GROUP 4: NODE20 SCENARIOS (Node20_1HandlerData) + // ======================================================================================== + new TestScenario( + name: "Node20_DefaultBehavior_WithHandler", + description: "Node20 handler uses Node20 by default", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { }, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Node20_WithGlobalUseNode20Knob", + description: "Global Node20 knob forces Node20 regardless of handler type", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { ["AGENT_USE_NODE20_1"] = "true" }, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Node20_GlibcError_EOLPolicy_UpgradesToNode24", + description: "Node20 with glibc error and EOL policy: legacy falls back to Node16, strategy-based upgrades to Node24", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + node20GlibcError: true, + legacyExpectedNode: "node16", + strategyExpectedNode: "node24", + strategyExpectedWarning: "" + ), + + new TestScenario( + name: "Node20_WithGlobalUseNode24Knob", + description: "Global Node24 knob overrides Node20 handler data", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { ["AGENT_USE_NODE24"] = "true" }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node20_WithUseNode10Knob", + description: "Node20 handler ignores deprecated Node10 knob in strategy-based approach", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { ["AGENT_USE_NODE10"] = "true" }, + legacyExpectedNode: "node10", + strategyExpectedNode: "node20_1" + ), + + new TestScenario( + name: "Node20_MultipleKnobs_GlobalWins", + description: "Global Node24 knob takes highest priority when multiple knobs are set with Node20 handler", + handlerData: typeof(Node20_1HandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "true", + ["AGENT_USE_NODE20_1"] = "true", + ["AGENT_USE_NODE24"] = "true" + }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node20_GlibcError_Node24GlibcError_EOLPolicy_ThrowsError", + description: "Node20 and Node24 with glibc error and EOL policy enabled throws error (cannot fallback to Node16), legacy picks Node16", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + node20GlibcError: true, + node24GlibcError: true, + legacyExpectedNode: "node16", + expectedErrorType: typeof(NotSupportedException), + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node20_1HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" + ), + + new TestScenario( + name: "Node20_PriorityTest_UseNode20OverridesUseNode10", + description: "Node20 global knob takes priority over Node10 global knob", + handlerData: typeof(Node20_1HandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "true", + ["AGENT_USE_NODE20_1"] = "true" + }, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Node20_PriorityTest_UseNode24OverridesUseNode20", + description: "Node24 global knob takes priority over Node20 global knob", + handlerData: typeof(Node20_1HandlerData), + knobs: new() + { + ["AGENT_USE_NODE20_1"] = "true", + ["AGENT_USE_NODE24"] = "true" + }, + expectedNode: "node24" + ), + + // ======================================================================================== + // GROUP 5: CONTAINER-SPECIFIC EOL SCENARIOS + // ======================================================================================== + + new TestScenario( + name: "Node20_AllGlobalKnobsDisabled_UsesHandler", + description: "Node20 handler uses handler data when all global knobs are disabled", + handlerData: typeof(Node20_1HandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "false", + ["AGENT_USE_NODE20_1"] = "false", + ["AGENT_USE_NODE24"] = "false" + }, + expectedNode: "node20_1" + ), + + + // ======================================================================================== + // GROUP 6: NODE24 SCENARIOS (Node24HandlerData) + // ======================================================================================== + + new TestScenario( + name: "Node24_DefaultBehavior_WithKnobEnabled", + description: "Node24 handler uses Node24 when handler-specific knob is enabled", + handlerData: typeof(Node24HandlerData), + knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true" }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node24_WithHandlerDataKnobDisabled_FallsBackToNode20", + description: "Node24 handler falls back to Node20 when AGENT_USE_NODE24_WITH_HANDLER_DATA=false", + handlerData: typeof(Node24HandlerData), + knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "false" }, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Node24_WithGlobalUseNode24Knob", + description: "Global Node24 knob overrides handler-specific knob setting", + handlerData: typeof(Node24HandlerData), + knobs: new() { ["AGENT_USE_NODE24"] = "true" }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node24_WithUseNode10Knob", + description: "Node24 handler ignores deprecated Node10 knob in strategy-based approach", + handlerData: typeof(Node24HandlerData), + knobs: new() + { + ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true", + ["AGENT_USE_NODE10"] = "true" + }, + legacyExpectedNode: "node10", + strategyExpectedNode: "node24" + ), + + new TestScenario( + name: "Node24_WithUseNode20Knob", + description: "Node24 handler ignores deprecated Node20 knob in strategy-based approach", + handlerData: typeof(Node24HandlerData), + knobs: new() + { + ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true", + ["AGENT_USE_NODE20_1"] = "true" + }, + legacyExpectedNode: "node20_1", + strategyExpectedNode: "node24" + ), + + new TestScenario( + name: "Node24_GlibcError_FallsBackToNode20", + description: "Node24 with glibc compatibility error falls back to Node20", + handlerData: typeof(Node24HandlerData), + knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true" }, + node24GlibcError: true, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Node24_GlibcError_Node20GlibcError_FallsBackToNode16", + description: "Node24 with both Node24 and Node20 glibc errors falls back to Node16", + handlerData: typeof(Node24HandlerData), + knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true" }, + node24GlibcError: true, + node20GlibcError: true, + expectedNode: "node16" + ), + + new TestScenario( + name: "Node24_GlibcError_Node20GlibcError_EOLPolicy_ThrowsError", + description: "Node24 with all glibc errors and EOL policy throws error (strategy-based) or falls back to Node16 (legacy)", + handlerData: typeof(Node24HandlerData), + knobs: new() + { + ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true", + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" + }, + node24GlibcError: true, + node20GlibcError: true, + legacyExpectedNode: "node16", + expectedErrorType: typeof(NotSupportedException), + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node24HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" + ), + + new TestScenario( + name: "Node24_PriorityTest_UseNode24OverridesUseNode20", + description: "Node24 global knob takes priority over Node20 global knob", + handlerData: typeof(Node24HandlerData), + knobs: new() + { + ["AGENT_USE_NODE20_1"] = "true", + ["AGENT_USE_NODE24"] = "true" + }, + expectedNode: "node24" + ), + + new TestScenario( + name: "Node24NotExecutable_fallsBackToNode20_1", + description: "Node24 handler with Node24 not executable: falls back to Node20_1 in container", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true" }, + node24Executable: false, + expectedNode: "node20_1" + ), + + // ======================================================================================== + // GROUP 7: EDGE CASES AND ERROR SCENARIOS + // ======================================================================================== + + new TestScenario( + name: "Node16_EOLPolicy_WithUseNode10Knob_UpgradesToNode24", + description: "Node16 handler with deprecated Node10 knob upgrades to Node24 when EOL policy is enabled (strategy-based) or uses Node10 (legacy)", + handlerData: typeof(Node16HandlerData), + knobs: new() + { + ["AGENT_USE_NODE10"] = "true", + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" + }, + legacyExpectedNode: "node10", + strategyExpectedNode: "node24", + strategyExpectedWarning: "NodeEOLUpgradeWarning" + ), + + // ======================================================================================== + // GROUP 8: CONTAINER SCENARIOS + // ======================================================================================== + + new TestScenario( + name: "CustomNode_Container_OverridesHandlerData", + description: "Container custom node path overrides task handler data", + handlerData: typeof(Node24HandlerData), + customNodePath: "/container/node20/bin/node", + inContainer: true, + expectedNode: "/container/node20/bin/node" + ), + + new TestScenario( + name: "CustomNode_Container_OverridesContainerKnobs", + description: "Container custom node path overrides container-specific knobs", + handlerData: typeof(Node20_1HandlerData), + knobs: new() + { + ["AZP_AGENT_USE_NODE24_TO_START_CONTAINER"] = "true", + ["AZP_AGENT_USE_NODE20_TO_START_CONTAINER"] = "true" + }, + customNodePath: "/container/custom/node", + inContainer: true, + expectedNode: "/container/custom/node" + ), + + new TestScenario( + name: "CustomNode_Container_OverridesContainerNode20Knobs", + description: "Container custom node path overrides node20 knob to start container", + handlerData: typeof(Node20_1HandlerData), + knobs: new() + { + ["AZP_AGENT_USE_NODE20_TO_START_CONTAINER"] = "true" + }, + customNodePath: "/container/custom/node", + inContainer: true, + expectedNode: "/container/custom/node" + ), + + new TestScenario( + name: "CustomNode_Container_OverridesContainerNode24Knobs", + description: "Container custom node path overrides node24 knob to start container", + handlerData: typeof(Node20_1HandlerData), + knobs: new() + { + ["AZP_AGENT_USE_NODE24_TO_START_CONTAINER"] = "true", + }, + customNodePath: "/container/custom/node", + inContainer: true, + expectedNode: "/container/custom/node" + ), + + new TestScenario( + name: "Container_EOLPolicyDisabled_AllowsNode16Fallback", + description: "Container with EOL policy disabled allows fallback to Node16 when container knobs are disabled", + handlerData: typeof(Node16HandlerData), + knobs: new() { + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false", + ["AZP_AGENT_USE_NODE20_TO_START_CONTAINER"] = "false", + ["AZP_AGENT_USE_NODE24_TO_START_CONTAINER"] = "false" + }, + expectedNode: "node16", + inContainer: true + ), + + new TestScenario( + name: "Container_EOLPolicy_UpgradesToNode24", + description: "Container with EOL policy upgrades to Node24 when Node24 container knob is enabled", + handlerData: typeof(Node16HandlerData), + knobs: new() { + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true", + ["AZP_AGENT_USE_NODE24_TO_START_CONTAINER"] = "true" + }, + expectedNode: "node24", + inContainer: true + ), + + new TestScenario( + name: "Container_Node20Enabled_DefaultBehavior", + description: "Container with Node20 enabled works correctly when Node24 is disabled", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { + ["AZP_AGENT_USE_NODE20_TO_START_CONTAINER"] = "true", + ["AZP_AGENT_USE_NODE24_TO_START_CONTAINER"] = "false" + }, + expectedNode: "node20_1", + inContainer: true + ), + + new TestScenario( + name: "Container_Node24Enabled_DefaultBehavior", + description: "Container with Node24 enabled works correctly", + handlerData: typeof(Node24HandlerData), + knobs: new() { + ["AZP_AGENT_USE_NODE24_TO_START_CONTAINER"] = "true" + }, + expectedNode: "node24", + inContainer: true + ), + + new TestScenario( + name: "Container_EOLPolicy_Node24Preferred_GlibcError_FallsBackToNode20", + description: "Container with EOL policy, Node24 preferred but has glibc error: falls back to Node20", + handlerData: typeof(Node16HandlerData), + knobs: new() { + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true", + ["AZP_AGENT_USE_NODE20_TO_START_CONTAINER"] = "true", + ["AZP_AGENT_USE_NODE24_TO_START_CONTAINER"] = "true" + }, + node24GlibcError: true, + inContainer: true, + expectedNode: "node20_1" + ), + + new TestScenario( + name: "Container_Node20Preferred_GlibcError_FallsBackToNode16", + description: "Container with Node20 preferred but has glibc error: falls back to Node16 when EOL policy disabled", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false", + ["AZP_AGENT_USE_NODE20_TO_START_CONTAINER"] = "true", + ["AZP_AGENT_USE_NODE24_TO_START_CONTAINER"] = "false" + }, + node20GlibcError: true, + expectedNode: "node16", + inContainer: true + ), + + new TestScenario( + name: "Container_Node24Enabled_GlibcError_EOLPolicy_FallsBackToNode20", + description: "Container with Node24 enabled but has glibc error: falls back to Node20 when EOL policy prevents Node16", + handlerData: typeof(Node24HandlerData), + knobs: new() + { + ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true", + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true", + ["AZP_AGENT_USE_NODE20_TO_START_CONTAINER"] = "true" + }, + node24GlibcError: true, + expectedNode: "node20_1", + inContainer: true + ), + + new TestScenario( + name: "Container_EOLPolicy_AllModernNodesFailGlibc_ThrowsError", + description: "Container with EOL policy and both Node24/Node20 glibc errors: cannot use Node16 due to policy, throws error", + handlerData: typeof(Node16HandlerData), + knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, + node24GlibcError: true, + node20GlibcError: true, + inContainer: true, + legacyExpectedNode: "node16", + expectedErrorType: typeof(NotSupportedException), + strategyExpectedError: "No compatible Node.js version available for container execution. Node16 is blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks." + ), + + new TestScenario( + name: "Container_AllModernVersionsFailGlibc_EOLPolicy_ThrowsError", + description: "Container with all modern Node.js versions having glibc errors and EOL policy: throws error (strategy-based) or falls back to Node16 (legacy)", + handlerData: typeof(Node24HandlerData), + knobs: new() + { + ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true", + ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" + }, + node24GlibcError: true, + node20GlibcError: true, + legacyExpectedNode: "node16", + expectedErrorType: typeof(NotSupportedException), + strategyExpectedError: "No compatible Node.js version available for container execution. Node16 is blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks.", + inContainer: true + ), + + new TestScenario( + name: "Container_GlobalNode24Knob_OverridesContainerDefaults", + description: "Global Node24 knob with container Node24 knob enabled uses Node24 in container", + handlerData: typeof(Node20_1HandlerData), + knobs: new() { + ["AGENT_USE_NODE24"] = "true", + ["AZP_AGENT_USE_NODE24_TO_START_CONTAINER"] = "true" + }, + expectedNode: "node24", + inContainer: true + ) + }; + } + + /// + /// Test scenario specification. + /// + public class TestScenario + { + // Identification + public string Name { get; set; } + public string Description { get; set; } + + // Test inputs - Handler Configuration + public Type HandlerDataType { get; set; } + + public Dictionary Knobs { get; set; } = new(); + public bool Node20GlibcError { get; set; } + public bool Node24GlibcError { get; set; } + public bool InContainer { get; set; } + public string CustomNodePath { get; set; } + public bool Node24Executable { get; set; } + + // Expected results (for equivalent scenarios) + public string ExpectedNode { get; set; } + + // Expected results (for divergent scenarios) + public string LegacyExpectedNode { get; set; } + public string StrategyExpectedNode { get; set; } + public string StrategyExpectedError { get; set; } + public string StrategyExpectedWarning { get; set; } + public Type ExpectedErrorType { get; set; } + + public TestScenario( + string name, + string description, + Type handlerData, + Dictionary knobs = null, + string expectedNode = null, + string legacyExpectedNode = null, + string strategyExpectedNode = null, + string strategyExpectedError = null, + string strategyExpectedWarning = null, + Type expectedErrorType = null, + bool node20GlibcError = false, + bool node24GlibcError = false, + bool node24Executable = true, + bool inContainer = false, + string customNodePath = null + ) + { + Name = name; + Description = description; + HandlerDataType = handlerData ?? throw new ArgumentNullException(nameof(handlerData)); + Knobs = knobs ?? new Dictionary(); + ExpectedNode = expectedNode; + LegacyExpectedNode = legacyExpectedNode ?? expectedNode; + StrategyExpectedNode = strategyExpectedNode ?? expectedNode; + StrategyExpectedError = strategyExpectedError; + StrategyExpectedWarning = strategyExpectedWarning; + ExpectedErrorType = expectedErrorType; + Node20GlibcError = node20GlibcError; + Node24GlibcError = node24GlibcError; + Node24Executable = node24Executable; + InContainer = inContainer; + CustomNodePath = customNodePath; + } + } +} \ No newline at end of file diff --git a/src/Test/L0/NodeHandlerL0.cs b/src/Test/L0/NodeHandlerL0.cs index 9c39e37117..a56759662f 100644 --- a/src/Test/L0/NodeHandlerL0.cs +++ b/src/Test/L0/NodeHandlerL0.cs @@ -12,9 +12,13 @@ using Moq; using Xunit; using Agent.Sdk; +using System.Threading; +using System.Threading.Tasks; +using System.Text; namespace Microsoft.VisualStudio.Services.Agent.Tests { + [Collection("Unified NodeHandler Tests")] public sealed class NodeHandlerL0 { private Mock nodeHandlerHalper; @@ -85,6 +89,12 @@ public void UseNewNodeForNewNodeHandler(string nodeVersion) { thc.SetSingleton(new WorkerCommandManager() as IWorkerCommandManager); thc.SetSingleton(new ExtensionManager() as IExtensionManager); + var processInvokerMock = new Mock(); + for (int i = 0; i < 10; i++) + { + thc.EnqueueInstance(processInvokerMock.Object); + } + SetupNodeProcessInvocation(processInvokerMock, nodeVersion, true); NodeHandler nodeHandler = new NodeHandler(nodeHandlerHalper.Object); @@ -139,6 +149,12 @@ public void ForceUseNode24Knob(string nodeVersion) { thc.SetSingleton(new WorkerCommandManager() as IWorkerCommandManager); thc.SetSingleton(new ExtensionManager() as IExtensionManager); + var processInvokerMock = new Mock(); + for (int i = 0; i < 10; i++) + { + thc.EnqueueInstance(processInvokerMock.Object); + } + SetupNodeProcessInvocation(processInvokerMock, nodeVersion, true); NodeHandler nodeHandler = new NodeHandler(nodeHandlerHalper.Object); @@ -168,6 +184,64 @@ public void ForceUseNode24Knob(string nodeVersion) } } + [Theory] + [InlineData("node24")] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void Node24NotExecutable(string nodeVersion) + { + ResetNodeKnobs(); + + Environment.SetEnvironmentVariable("AGENT_USE_NODE24_WITH_HANDLER_DATA", "true"); + + try + { + // Use a unique test name per data row to avoid sharing the same trace file across parallel runs + using (TestHostContext thc = CreateTestHostContext($"{nameof(Node24NotExecutable)}_{nodeVersion}")) + { + thc.SetSingleton(new WorkerCommandManager() as IWorkerCommandManager); + thc.SetSingleton(new ExtensionManager() as IExtensionManager); + var processInvokerMock = new Mock(); + for (int i = 0; i < 10; i++) + { + thc.EnqueueInstance(processInvokerMock.Object); + } + SetupNodeProcessInvocation(processInvokerMock, nodeVersion, false); + + nodeHandlerHalper + .Setup(x => x.IsNodeExecutable( + It.Is(folder => folder == "node24"), + It.IsAny(), + It.IsAny())) + .Returns(false); + NodeHandler nodeHandler = new NodeHandler(nodeHandlerHalper.Object); + + nodeHandler.Initialize(thc); + nodeHandler.ExecutionContext = CreateTestExecutionContext(thc); + nodeHandler.Data = nodeVersion switch + { + "node" => new NodeHandlerData(), + "node10" => new Node10HandlerData(), + "node16" => new Node16HandlerData(), + "node20_1" => new Node20_1HandlerData(), + "node24" => new Node24HandlerData(), + _ => throw new Exception("Invalid node version"), + }; + + string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false); + string expectedLocation = Path.Combine(thc.GetDirectory(WellKnownDirectory.Externals), + "node20_1", + "bin", + $"node{IOUtil.ExeExtension}"); + Assert.Equal(expectedLocation, actualLocation); + } + } + finally + { + Environment.SetEnvironmentVariable("AGENT_USE_NODE24_WITH_HANDLER_DATA", null); + } + } + //tests that Node24 is NOT used when handler data exists but knob is false [Fact] [Trait("Level", "L0")] @@ -573,9 +647,28 @@ private Mock GetMockedNodeHandlerHelper() .Setup(x => x.GetFilteredPossibleNodeFolders(It.IsAny(), It.IsAny())) .Returns(Array.Empty); + nodeHandlerHelper + .Setup(x => x.IsNodeExecutable(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(true); + return nodeHandlerHelper; } + private void SetupNodeProcessInvocation(Mock processInvokerMock, string nodeFolder, bool node24Executable) + { + string nodeExePath = Path.Combine("externals", nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + + processInvokerMock.Setup(x => x.ExecuteAsync( + It.IsAny(), + It.Is(fileName => fileName.Contains(nodeExePath)), + "-v", + It.IsAny>(), + false, + It.IsAny(), + It.IsAny())) + .ReturnsAsync(node24Executable ? 0 : 216); + } + private void ResetNodeKnobs() { Environment.SetEnvironmentVariable("AGENT_USE_NODE10", null); diff --git a/src/Test/L0/NodeHandlerTestBase.cs b/src/Test/L0/NodeHandlerTestBase.cs new file mode 100644 index 0000000000..8382303de1 --- /dev/null +++ b/src/Test/L0/NodeHandlerTestBase.cs @@ -0,0 +1,514 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; +using Microsoft.VisualStudio.Services.Agent.Worker.Handlers; +using Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies; +using Moq; +using Xunit; +using Agent.Sdk; + +namespace Microsoft.VisualStudio.Services.Agent.Tests +{ + public abstract class NodeHandlerTestBase : IDisposable + { + protected Mock NodeHandlerHelper { get; private set; } + protected List CapturedWarnings { get; private set; } = new List(); + private bool disposed = false; + + protected NodeHandlerTestBase() + { + NodeHandlerHelper = GetMockedNodeHandlerHelper(); + ResetEnvironment(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + ResetEnvironment(); + } + disposed = true; + } + } + + protected void RunScenarioAndAssert(TestScenario scenario, bool useStrategy) + { + ResetEnvironment(); + + foreach (var knob in scenario.Knobs) + { + Environment.SetEnvironmentVariable(knob.Key, knob.Value); + } + + Environment.SetEnvironmentVariable("AGENT_USE_ENHANCED_NODE_SELECTION", useStrategy ? "true" : "false"); + + try + { + using (TestHostContext thc = new TestHostContext(this, scenario.Name)) + { + thc.SetSingleton(new WorkerCommandManager() as IWorkerCommandManager); + thc.SetSingleton(new ExtensionManager() as IExtensionManager); + + var glibcCheckerMock = SetupMockedGlibcCompatibilityInfoProvider(scenario); + thc.SetSingleton(glibcCheckerMock.Object); + + var dockerManagerMock = SetupMockedDockerCommandManager(scenario); + thc.SetSingleton(dockerManagerMock.Object); + + // Mock IProcessInvoker for node executable checks (e.g., IsNodeExecutable in Node24Strategy) + var processInvokerMock = new Mock(); + for (int i = 0; i < 10; i++) + { + thc.EnqueueInstance(processInvokerMock.Object); + } + + SetupNodeProcessInvocation(processInvokerMock, scenario.HandlerDataType.Name, scenario.Node24Executable); + + var expectations = GetScenarioExpectations(scenario, useStrategy); + try{ + string actualLocation; + + if (scenario.InContainer) + { + actualLocation = TestActualContainerNodeSelection(thc, scenario); + } + else + { + ConfigureNodeHandlerHelper(scenario); + + NodeHandler nodeHandler = new NodeHandler(NodeHandlerHelper.Object); + nodeHandler.Initialize(thc); + + var executionContextMock = CreateTestExecutionContext(thc, scenario); + nodeHandler.ExecutionContext = executionContextMock.Object; + nodeHandler.Data = CreateHandlerData(scenario.HandlerDataType); + + actualLocation = nodeHandler.GetNodeLocation( + node20ResultsInGlibCError: scenario.Node20GlibcError, + node24ResultsInGlibCError: scenario.Node24GlibcError, + inContainer: false); + } + + string expectedLocation = GetExpectedNodeLocation(expectations.ExpectedNode, scenario, thc); + Assert.Equal(expectedLocation, actualLocation); + + // Assert warning expectations for strategy-based mode + if (useStrategy && scenario.StrategyExpectedWarning != null) + { + if (string.IsNullOrEmpty(scenario.StrategyExpectedWarning)) + { + Assert.DoesNotContain(CapturedWarnings, w => w.Contains("NodeEOLUpgradeWarning")); + } + else + { + Assert.Contains(CapturedWarnings, w => w.Contains(scenario.StrategyExpectedWarning)); + } + } + } + catch (Exception ex) + { + Assert.NotNull(ex); + Assert.IsType(scenario.ExpectedErrorType, ex); + + if (!string.IsNullOrEmpty(expectations.ExpectedError)) + { + Assert.Contains(expectations.ExpectedError, ex.Message); + } + } + } + } + finally + { + ResetEnvironment(); + } + } + + /// + /// Sets up a mocked GlibcCompatibilityInfoProvider for focused NodeHandler testing. + /// + private Mock SetupMockedGlibcCompatibilityInfoProvider(TestScenario scenario) + { + var glibcCheckerMock = new Mock(); + + var glibcInfo = GlibcCompatibilityInfo.Create( + scenario.Node24GlibcError, + scenario.Node20GlibcError); + + glibcCheckerMock + .Setup(x => x.Initialize(It.IsAny())); + + glibcCheckerMock + .Setup(x => x.CheckGlibcCompatibilityAsync(It.IsAny())) + .ReturnsAsync(glibcInfo); + + glibcCheckerMock + .Setup(x => x.GetGlibcCompatibilityAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(glibcInfo); + + return glibcCheckerMock; + } + + /// + /// Sets up a mocked DockerCommandManager for container scenarios in NodeHandler testing. + /// + private Mock SetupMockedDockerCommandManager(TestScenario scenario) + { + var dockerManagerMock = new Mock(); + + dockerManagerMock + .Setup(x => x.DockerInspect(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync("mocked_inspect_result"); + + dockerManagerMock + .Setup(x => x.DockerVersion(It.IsAny())) + .ReturnsAsync(new DockerVersion(new Version("1.0.0"), new Version("1.0.0"))); + + dockerManagerMock + .Setup(x => x.IsContainerRunning(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + dockerManagerMock + .Setup(x => x.DockerExec(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns>((execContext, containerId, workDir, command, output) => + { + if (command.Contains("node") && command.Contains("--version")) + { + bool isNode24 = command.Contains("node24"); + bool isNode20 = command.Contains("node20_1"); + bool isNode16 = command.Contains("node16"); + + bool hasGlibcError = (isNode24 && scenario.Node24GlibcError) || + (isNode20 && scenario.Node20GlibcError); + + if (hasGlibcError) + { + return Task.FromResult(127); + } + else + { + if (isNode24) output.Add("v24.0.0"); + else if (isNode20) output.Add("v20.1.0"); + else if (isNode16) output.Add("v16.20.2"); + else output.Add("v20.1.0"); + return Task.FromResult(0); + } + } + return Task.FromResult(127); + }); + + return dockerManagerMock; + } + + private string TestActualContainerNodeSelection(TestHostContext thc, TestScenario scenario) + { + try + { + var executionContextMock = CreateTestExecutionContext(thc, scenario); + var orchestrator = new NodeVersionOrchestrator(executionContextMock.Object, thc, NodeHandlerHelper.Object); + var taskContext = new TaskContext + { + HandlerData = CreateHandlerData(scenario.HandlerDataType), + Container = new ContainerInfo + { + ContainerId = "test_container", + CustomNodePath = scenario.CustomNodePath, + IsJobContainer = true, + ImageOS = PlatformUtil.RunningOnMacOS ? PlatformUtil.OS.OSX : + PlatformUtil.RunningOnWindows ? PlatformUtil.OS.Windows : PlatformUtil.OS.Linux + }, + StepTarget = !string.IsNullOrWhiteSpace(scenario.CustomNodePath) + ? new ContainerInfo { CustomNodePath = scenario.CustomNodePath } + : null + }; + + var dockerManager = thc.GetService(); + var result = orchestrator.SelectNodeVersionForContainer(taskContext, dockerManager); + return result.NodePath; + } + catch (Exception ex) + { + Console.WriteLine($"TestActualContainerNodeSelection error: {ex}"); + throw; + } + } + + private void ConfigureNodeHandlerHelper(TestScenario scenario) + { + NodeHandlerHelper.Reset(); + + NodeHandlerHelper + .Setup(x => x.IsNodeFolderExist(It.IsAny(), It.IsAny())) + .Returns(true); + + NodeHandlerHelper + .Setup(x => x.GetNodeFolderPath(It.IsAny(), It.IsAny())) + .Returns((string nodeFolderName, IHostContext hostContext) => Path.Combine( + hostContext.GetDirectory(WellKnownDirectory.Externals), + nodeFolderName, + "bin", + $"node{IOUtil.ExeExtension}")); + NodeHandlerHelper + .Setup(x => x.IsNodeExecutable(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(scenario.Node24Executable); + } + + private string GetExpectedNodeLocation(string expectedNode, TestScenario scenario, TestHostContext thc) + { + if (!string.IsNullOrWhiteSpace(scenario.CustomNodePath)) + { + return scenario.CustomNodePath; + } + + if (scenario.InContainer) + { + string hostPath = Path.Combine( + thc.GetDirectory(WellKnownDirectory.Externals), + expectedNode, + "bin", + $"node{IOUtil.ExeExtension}"); + + var containerInfo = new ContainerInfo + { + ContainerId = "test_container", + IsJobContainer = true, + ImageOS = PlatformUtil.RunningOnMacOS ? PlatformUtil.OS.OSX : + PlatformUtil.RunningOnWindows ? PlatformUtil.OS.Windows : PlatformUtil.OS.Linux + }; + + string containerPath = containerInfo.TranslateToContainerPath(hostPath); + + string containerExeExtension = containerInfo.ImageOS == PlatformUtil.OS.Windows ? ".exe" : ""; + string finalPath = containerPath.Replace($"node{IOUtil.ExeExtension}", $"node{containerExeExtension}"); + + return finalPath; + } + else + { + return Path.Combine( + thc.GetDirectory(WellKnownDirectory.Externals), + expectedNode, + "bin", + $"node{IOUtil.ExeExtension}"); + } + } + + protected ScenarioExpectations GetScenarioExpectations(TestScenario scenario, bool useStrategy) + { + // Check if this is an equivalent scenario by seeing if strategy-specific fields are populated + bool isEquivalentScenario = string.IsNullOrEmpty(scenario.StrategyExpectedNode) && + string.IsNullOrEmpty(scenario.LegacyExpectedNode); + + if (isEquivalentScenario) + { + // Equivalent scenarios: same behavior for both modes, use shared ExpectedNode + return new ScenarioExpectations + { + ExpectedNode = scenario.ExpectedNode, + ExpectedError = null + }; + } + else + { + // Divergent scenarios: different behavior between legacy and strategy + if (useStrategy) + { + return new ScenarioExpectations + { + ExpectedNode = scenario.StrategyExpectedNode, + ExpectedError = scenario.StrategyExpectedError + }; + } + else + { + return new ScenarioExpectations + { + ExpectedNode = scenario.LegacyExpectedNode, + ExpectedError = null + }; + } + } + } + + protected BaseNodeHandlerData CreateHandlerData(Type handlerDataType) + { + if (handlerDataType == typeof(NodeHandlerData)) + return new NodeHandlerData(); + else if (handlerDataType == typeof(Node10HandlerData)) + return new Node10HandlerData(); + else if (handlerDataType == typeof(Node16HandlerData)) + return new Node16HandlerData(); + else if (handlerDataType == typeof(Node20_1HandlerData)) + return new Node20_1HandlerData(); + else if (handlerDataType == typeof(Node24HandlerData)) + return new Node24HandlerData(); + else + throw new ArgumentException($"Unknown handler data type: {handlerDataType}"); + } + + protected Mock CreateTestExecutionContext(TestHostContext tc, Dictionary knobs) + { + var executionContext = new Mock(); + var variables = new Dictionary(); + + foreach (var knob in knobs) + { + variables[knob.Key] = new VariableValue(knob.Value); + } + + List warnings; + executionContext + .Setup(x => x.Variables) + .Returns(new Variables(tc, copy: variables, warnings: out warnings)); + + executionContext + .Setup(x => x.GetScopedEnvironment()) + .Returns(new SystemEnvironment()); + + executionContext + .Setup(x => x.GetVariableValueOrDefault(It.IsAny())) + .Returns((string variableName) => + { + if (variables.TryGetValue(variableName, out VariableValue value)) + { + return value.Value; + } + return Environment.GetEnvironmentVariable(variableName); + }); + + executionContext + .Setup(x => x.GetHostContext()) + .Returns(tc); + + CapturedWarnings.Clear(); + executionContext + .Setup(x => x.AddIssue(It.Is(i => i.Type == IssueType.Warning))) + .Callback(issue => CapturedWarnings.Add(issue.Message)); + + return executionContext; + } + + protected Mock CreateTestExecutionContext(TestHostContext tc, TestScenario scenario) + { + var executionContext = CreateTestExecutionContext(tc, scenario.Knobs); + + if (!string.IsNullOrWhiteSpace(scenario.CustomNodePath)) + { + var stepTarget = CreateStepTargetObject(scenario); + executionContext + .Setup(x => x.StepTarget()) + .Returns(stepTarget); + } + else + { + executionContext + .Setup(x => x.StepTarget()) + .Returns((ExecutionTargetInfo)null); + } + + return executionContext; + } + + private ExecutionTargetInfo CreateStepTargetObject(TestScenario scenario) + { + if (scenario.InContainer) + { + return new ContainerInfo() + { + CustomNodePath = scenario.CustomNodePath + }; + } + else + { + return new HostInfo() + { + CustomNodePath = scenario.CustomNodePath + }; + } + } + + private Mock GetMockedNodeHandlerHelper() + { + var nodeHandlerHelper = new Mock(); + + nodeHandlerHelper + .Setup(x => x.IsNodeFolderExist(It.IsAny(), It.IsAny())) + .Returns(true); + + nodeHandlerHelper + .Setup(x => x.GetNodeFolderPath(It.IsAny(), It.IsAny())) + .Returns((string nodeFolderName, IHostContext hostContext) => Path.Combine( + hostContext.GetDirectory(WellKnownDirectory.Externals), + nodeFolderName, + "bin", + $"node{IOUtil.ExeExtension}")); + + return nodeHandlerHelper; + } + + protected void ResetEnvironment() + { + // Core Node.js strategy knobs + Environment.SetEnvironmentVariable("AGENT_USE_NODE10", null); + Environment.SetEnvironmentVariable("AGENT_USE_NODE20_1", null); + Environment.SetEnvironmentVariable("AGENT_USE_NODE24", null); + Environment.SetEnvironmentVariable("AGENT_USE_NODE24_WITH_HANDLER_DATA", null); + Environment.SetEnvironmentVariable("AGENT_USE_NODE", null); + Environment.SetEnvironmentVariable("AZP_AGENT_USE_NODE20_TO_START_CONTAINER", null); + Environment.SetEnvironmentVariable("AZP_AGENT_USE_NODE24_TO_START_CONTAINER", null); + + // EOL and strategy control + Environment.SetEnvironmentVariable("AGENT_RESTRICT_EOL_NODE_VERSIONS", null); + Environment.SetEnvironmentVariable("AGENT_USE_ENHANCED_NODE_SELECTION", null); + + // System-specific knobs + Environment.SetEnvironmentVariable("AGENT_USE_NODE20_IN_UNSUPPORTED_SYSTEM", null); + Environment.SetEnvironmentVariable("AGENT_USE_NODE24_IN_UNSUPPORTED_SYSTEM", null); + + } + + private void SetupNodeProcessInvocation(Mock processInvokerMock, string nodeFolder, bool node24Executable) + { + string nodeExePath = Path.Combine("externals", nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + + processInvokerMock.Setup(x => x.ExecuteAsync( + It.IsAny(), + It.Is(fileName => fileName.Contains(nodeExePath)), + "-v", + It.IsAny>(), + false, + It.IsAny(), + It.IsAny())) + .ReturnsAsync(node24Executable ? 0 : 216); + } + } + + public class TestResult + { + public string NodePath { get; set; } + public Exception Exception { get; set; } + } + + public class ScenarioExpectations + { + public string ExpectedNode { get; set; } + public string ExpectedError { get; set; } + } +} \ No newline at end of file diff --git a/src/Test/L0/Plugin/TestGitSourceProvider/GitSourceProviderL0.cs b/src/Test/L0/Plugin/TestGitSourceProvider/GitSourceProviderL0.cs index 39e975550a..6f65f8869e 100644 --- a/src/Test/L0/Plugin/TestGitSourceProvider/GitSourceProviderL0.cs +++ b/src/Test/L0/Plugin/TestGitSourceProvider/GitSourceProviderL0.cs @@ -9,6 +9,7 @@ using Moq; using Agent.Plugins.Repository; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.TeamFoundation.DistributedTask.WebApi; using Pipelines = Microsoft.TeamFoundation.DistributedTask.Pipelines; using Microsoft.VisualStudio.Services.Agent.Util; @@ -32,7 +33,7 @@ public sealed class TestPluginGitSourceProviderL0 [Trait("SkipOn", "darwin")] [Trait("SkipOn", "linux")] [MemberData(nameof(FeatureFlagsStatusData))] - public void TestSetGitConfiguration(bool featureFlagsStatus) + public async Task TestSetGitConfiguration(bool featureFlagsStatus) { using TestHostContext hc = new(this, $"FeatureFlagsStatus_{featureFlagsStatus}"); MockAgentTaskPluginExecutionContext tc = new(hc.GetTrace()); @@ -47,7 +48,7 @@ public void TestSetGitConfiguration(bool featureFlagsStatus) tc.Variables.Add("FIX_POSSIBLE_GIT_OUT_OF_MEMORY_PROBLEM", featureFlagStatusString); Agent.Plugins.Repository.GitSourceProvider gitSourceProvider = new Agent.Plugins.Repository.ExternalGitSourceProvider(); - gitSourceProvider.SetGitFeatureFlagsConfiguration(tc, gitCliManagerMock.Object, repositoryPath); + await gitSourceProvider.SetGitFeatureFlagsConfiguration(tc, gitCliManagerMock.Object, repositoryPath); // Assert. gitCliManagerMock.Verify(x => x.GitConfig(tc, repositoryPath, "pack.threads", "1"), invocation); diff --git a/src/Test/L0/ProcessInvokerL0.cs b/src/Test/L0/ProcessInvokerL0.cs index fcdcc402f8..3ad5d1f7ef 100644 --- a/src/Test/L0/ProcessInvokerL0.cs +++ b/src/Test/L0/ProcessInvokerL0.cs @@ -447,5 +447,121 @@ public async Task EnableWorkerCommandsByDefault() } } } + + // Race condition tests for ICM 698323303: Process exits during cancellation + // Validates that the try/catch in the cancellation callback (line 335-343) + // properly handles InvalidOperationException when _proc is disposed during cancel. + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public async Task TestCancel_ProcessExitsBeforeCancellation_NoException() + { + // Start a process that exits instantly, then cancel after a small delay. + // Validates: no InvalidOperationException when cancel fires on an already-exited process. + using (TestHostContext hc = new TestHostContext(this)) + using (var tokenSource = new CancellationTokenSource()) + { + Tracing trace = hc.GetTrace(); + using (var processInvoker = new ProcessInvokerWrapper()) + { + processInvoker.Initialize(hc); + + // Start a process that exits immediately + var execTask = (TestUtil.IsWindows()) + ? processInvoker.ExecuteAsync("", "cmd.exe", "/c exit 0", null, false, null, false, null, false, false, false, false, tokenSource.Token) + : processInvoker.ExecuteAsync("", "bash", "-c \"exit 0\"", null, false, null, false, null, false, false, false, false, tokenSource.Token); + + // Small delay to let process exit, then cancel + await Task.Delay(500); + tokenSource.Cancel(); + + // Should complete without throwing + int exitCode = await execTask; + trace.Info($"Exit Code: {exitCode}"); + Assert.Equal(0, exitCode); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public async Task TestCancel_ProcessAlreadyExited_NoException() + { + // Start a process that exits instantly, wait for completion, then fire cancellation. + // Validates that the cancellation callback handles an already-exited process gracefully. + using (TestHostContext hc = new TestHostContext(this)) + using (var tokenSource = new CancellationTokenSource()) + { + Tracing trace = hc.GetTrace(); + using (var processInvoker = new ProcessInvokerWrapper()) + { + processInvoker.Initialize(hc); + + // Start a process that exits immediately with a cancellation token + var execTask = (TestUtil.IsWindows()) + ? processInvoker.ExecuteAsync("", "cmd.exe", "/c exit 0", null, false, null, false, null, false, false, false, false, tokenSource.Token) + : processInvoker.ExecuteAsync("", "bash", "-c \"exit 0\"", null, false, null, false, null, false, false, false, false, tokenSource.Token); + + // Wait for the process to complete + int exitCode = await execTask; + trace.Info($"Exit Code: {exitCode}"); + Assert.Equal(0, exitCode); + + // Now cancel — the callback will attempt CancelAndKillProcessTree on an exited process. + // This should not throw any unhandled exceptions. + tokenSource.Cancel(); + + // If we get here without crashing, the test passes + trace.Info("Cancellation after process exit completed without exception."); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public async Task TestCancel_ConcurrentExitAndCancel_NoException() + { + // Stress test: start a very short-lived process and cancel immediately. + // Run multiple iterations to maximize the chance of hitting the race window. + // Validates: no unhandled exception regardless of timing. + using (TestHostContext hc = new TestHostContext(this)) + { + Tracing trace = hc.GetTrace(); + + for (int i = 0; i < 10; i++) + { + using (var tokenSource = new CancellationTokenSource()) + using (var processInvoker = new ProcessInvokerWrapper()) + { + processInvoker.Initialize(hc); + + // Start a process that exits very quickly + var execTask = (TestUtil.IsWindows()) + ? processInvoker.ExecuteAsync("", "cmd.exe", "/c ping 127.0.0.1 -n 1 > nul", null, false, null, false, null, false, false, false, false, tokenSource.Token) + : processInvoker.ExecuteAsync("", "bash", "-c \"sleep 0.05\"", null, false, null, false, null, false, false, false, false, tokenSource.Token); + + // Cancel almost immediately to race with process exit + await Task.Delay(30); + tokenSource.Cancel(); + + // Should complete without crashing — either process exits first or cancel succeeds + try + { + int exitCode = await execTask; + trace.Info($"Iteration {i}: Exit Code: {exitCode}"); + } + catch (OperationCanceledException) + { + // Expected if cancellation wins the race + trace.Info($"Iteration {i}: OperationCanceledException (expected)"); + } + // Any other exception (InvalidOperationException, NullReferenceException) would fail the test + } + } + } + } } } diff --git a/src/Test/L0/ServiceInterfacesL0.cs b/src/Test/L0/ServiceInterfacesL0.cs index 6dd1727762..92b0575fb3 100644 --- a/src/Test/L0/ServiceInterfacesL0.cs +++ b/src/Test/L0/ServiceInterfacesL0.cs @@ -21,6 +21,7 @@ using Microsoft.VisualStudio.Services.Agent.Worker.Release.ContainerFetchEngine; using Microsoft.VisualStudio.Services.Agent.Worker.Maintenance; using Microsoft.VisualStudio.Services.Agent.Listener.Diagnostics; +using Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies; namespace Microsoft.VisualStudio.Services.Agent.Tests { @@ -101,7 +102,8 @@ public void WorkerInterfacesSpecifyDefaultImplementation() typeof(INUnitResultsXmlReader), typeof(IWorkerCommand), typeof(ITaskRestrictionsChecker), - typeof(IRetryOptions) + typeof(IRetryOptions), + typeof(INodeVersionStrategy) }; Validate( assembly: typeof(IStepsRunner).GetTypeInfo().Assembly, diff --git a/src/Test/L0/TestHostContext.cs b/src/Test/L0/TestHostContext.cs index 59e3c99e14..2cf1d0e8a9 100644 --- a/src/Test/L0/TestHostContext.cs +++ b/src/Test/L0/TestHostContext.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.Services.Agent.Util; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Runtime.CompilerServices; @@ -37,6 +38,8 @@ public sealed class TestHostContext : IHostContext, IDisposable private Tracing _trace; private AssemblyLoadContext _loadContext; private string _tempDirectoryRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("D")); + public bool UseRealDelays { get; set; } = false; // Default: skip delays for speed + public List CapturedDelays { get; private set; } = new List(); private StartupType _startupType; public event EventHandler Unloading; public CancellationToken AgentShutdownToken => _agentShutdownTokenSource.Token; @@ -154,6 +157,14 @@ public StartupType StartupType public async Task Delay(TimeSpan delay, CancellationToken token) { + // Always capture the delay value for testing + CapturedDelays.Add(delay); + + if (UseRealDelays) + { + await Task.Delay(delay, token); + return; + } await Task.Delay(TimeSpan.Zero); } diff --git a/src/Test/L0/Util/CertificateUtilL0.cs b/src/Test/L0/Util/CertificateUtilL0.cs new file mode 100644 index 0000000000..678e6d6249 --- /dev/null +++ b/src/Test/L0/Util/CertificateUtilL0.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Agent.Sdk.Util; +using Xunit; + +namespace Microsoft.VisualStudio.Services.Agent.Tests.Util +{ + /// + /// Tests for CertificateUtil.LoadCertificate which works on both .NET 8 and .NET 10. + /// Tests cover: Cert (DER/PEM) and PFX/PKCS#12 formats. + /// + public sealed class CertificateUtilL0 : IDisposable + { + private readonly string _tempDir; + + public CertificateUtilL0() + { + _tempDir = Path.Combine(Path.GetTempPath(), $"CertUtilTests_{Guid.NewGuid():N}"); + Directory.CreateDirectory(_tempDir); + } + + public void Dispose() + { + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, recursive: true); + } + } + + #region PFX/PKCS#12 Tests (X509ContentType.Pkcs12) + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void LoadCertificate_Pfx_WithPassword_LoadsSuccessfully() + { + // Arrange + var (expectedThumbprint, pfxPath) = CreatePfxCertificate("test-password"); + + // Act + using var loadedCert = CertificateUtil.LoadCertificate(pfxPath, "test-password"); + + // Assert + Assert.NotNull(loadedCert); + Assert.Equal(expectedThumbprint, loadedCert.Thumbprint); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void LoadCertificate_Pfx_WithoutPassword_LoadsSuccessfully() + { + // Arrange + var (expectedThumbprint, pfxPath) = CreatePfxCertificate(password: null); + + // Act + using var loadedCert = CertificateUtil.LoadCertificate(pfxPath, password: null); + + // Assert + Assert.NotNull(loadedCert); + Assert.Equal(expectedThumbprint, loadedCert.Thumbprint); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void LoadCertificate_Pfx_WrongPassword_ThrowsException() + { + // Arrange + var (_, pfxPath) = CreatePfxCertificate("correct-password"); + + // Act & Assert + Assert.ThrowsAny(() => + CertificateUtil.LoadCertificate(pfxPath, "wrong-password")); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void LoadCertificate_Pfx_PasswordProtectedButNoPasswordProvided_ThrowsException() + { + // Arrange + var (_, pfxPath) = CreatePfxCertificate("some-password"); + + // Act & Assert + Assert.ThrowsAny(() => + CertificateUtil.LoadCertificate(pfxPath, password: null)); + } + + #endregion + + #region DER Tests (X509ContentType.Cert) + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void LoadCertificate_Der_LoadsSuccessfully() + { + // Arrange + var (expectedThumbprint, derPath) = CreateDerCertificate(); + + // Act + using var loadedCert = CertificateUtil.LoadCertificate(derPath); + + // Assert + Assert.NotNull(loadedCert); + Assert.Equal(expectedThumbprint, loadedCert.Thumbprint); + } + + #endregion + + #region PEM Tests (X509ContentType.Cert) + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void LoadCertificate_Pem_LoadsSuccessfully() + { + // Arrange + var (expectedThumbprint, pemPath) = CreatePemCertificate(); + + // Act + using var loadedCert = CertificateUtil.LoadCertificate(pemPath); + + // Assert + Assert.NotNull(loadedCert); + Assert.Equal(expectedThumbprint, loadedCert.Thumbprint); + } + + #endregion + + #region Helper Methods + + /// + /// Creates a test PFX/PKCS#12 certificate file (X509ContentType.Pkcs12). + /// + private (string thumbprint, string path) CreatePfxCertificate(string password) + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + "CN=TestPfxCertificate", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + using var cert = request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddMinutes(-5), + DateTimeOffset.UtcNow.AddYears(1)); + + var pfxPath = Path.Combine(_tempDir, $"test_{Guid.NewGuid():N}.pfx"); + var pfxBytes = cert.Export(X509ContentType.Pfx, password); + File.WriteAllBytes(pfxPath, pfxBytes); + + return (cert.Thumbprint, pfxPath); + } + + /// + /// Creates a test DER-encoded certificate file (X509ContentType.Cert). + /// + private (string thumbprint, string path) CreateDerCertificate() + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + "CN=TestDerCertificate", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + using var cert = request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddMinutes(-5), + DateTimeOffset.UtcNow.AddYears(1)); + + var derPath = Path.Combine(_tempDir, $"test_{Guid.NewGuid():N}.cer"); + var derBytes = cert.Export(X509ContentType.Cert); + File.WriteAllBytes(derPath, derBytes); + + return (cert.Thumbprint, derPath); + } + + /// + /// Creates a test PEM-encoded certificate file. + /// + private (string thumbprint, string path) CreatePemCertificate() + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + "CN=TestPemCertificate", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + using var cert = request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddMinutes(-5), + DateTimeOffset.UtcNow.AddYears(1)); + + var pemPath = Path.Combine(_tempDir, $"test_{Guid.NewGuid():N}.pem"); + var pemContent = cert.ExportCertificatePem(); + File.WriteAllText(pemPath, pemContent); + + return (cert.Thumbprint, pemPath); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Test/L0/Util/IOUtilL0.cs b/src/Test/L0/Util/IOUtilL0.cs index d561de67cb..dd2ee2faf8 100644 --- a/src/Test/L0/Util/IOUtilL0.cs +++ b/src/Test/L0/Util/IOUtilL0.cs @@ -85,7 +85,7 @@ public void Delete_DeletesFile() [Fact] [Trait("Level", "L0")] [Trait("Category", "Common")] - public async void DeleteDirectory_DeleteTargetFileWithASymlink() + public async Task DeleteDirectory_DeleteTargetFileWithASymlink() { using (TestHostContext hc = new TestHostContext(this)) { @@ -385,7 +385,7 @@ public void DeleteDirectory_DeletesReadOnlyRootDirectory() [Fact] [Trait("Level", "L0")] [Trait("Category", "Common")] - public async void DeleteDirectory_DeletesWithRetry_Success() + public async Task DeleteDirectory_DeletesWithRetry_Success() { using (TestHostContext hc = new TestHostContext(this)) { @@ -418,7 +418,7 @@ public async void DeleteDirectory_DeletesWithRetry_Success() [Trait("Category", "Common")] [Trait("SkipOn", "darwin")] [Trait("SkipOn", "linux")] - public async void DeleteDirectory_DeletesWithRetry_CancellationRequested() + public async Task DeleteDirectory_DeletesWithRetry_CancellationRequested() { using (TestHostContext hc = new TestHostContext(this)) { @@ -450,7 +450,7 @@ await Assert.ThrowsAsync(async () => [Fact] [Trait("Level", "L0")] [Trait("Category", "Common")] - public async void DeleteDirectory_DeletesWithRetry_NonExistenDir() + public async Task DeleteDirectory_DeletesWithRetry_NonExistenDir() { using (TestHostContext hc = new TestHostContext(this)) { @@ -470,7 +470,7 @@ public async void DeleteDirectory_DeletesWithRetry_NonExistenDir() [Trait("Category", "Common")] [Trait("SkipOn", "darwin")] [Trait("SkipOn", "linux")] - public async void DeleteDirectory_DeletesWithRetry_IOException() + public async Task DeleteDirectory_DeletesWithRetry_IOException() { using (TestHostContext hc = new TestHostContext(this)) { @@ -841,7 +841,7 @@ public void DeleteFile_IgnoresDirectory() [Fact] [Trait("Level", "L0")] [Trait("Category", "Common")] - public async void DeleteFile_DeletesWithRetry_Success() + public async Task DeleteFile_DeletesWithRetry_Success() { using (TestHostContext hc = new TestHostContext(this)) { @@ -873,7 +873,7 @@ public async void DeleteFile_DeletesWithRetry_Success() [Fact] [Trait("Level", "L0")] [Trait("Category", "Common")] - public async void DeleteFile_DeletesWithRetry_NonExistenFile() + public async Task DeleteFile_DeletesWithRetry_NonExistenFile() { using (TestHostContext hc = new TestHostContext(this)) { @@ -893,7 +893,7 @@ public async void DeleteFile_DeletesWithRetry_NonExistenFile() [Trait("Category", "Common")] [Trait("SkipOn", "darwin")] [Trait("SkipOn", "linux")] - public async void DeleteFile_DeletesWithRetry_IOException() + public async Task DeleteFile_DeletesWithRetry_IOException() { using (TestHostContext hc = new TestHostContext(this)) { @@ -922,7 +922,7 @@ await Assert.ThrowsAsync(async () => [Trait("Category", "Common")] [Trait("SkipOn", "darwin")] [Trait("SkipOn", "linux")] - public async void DeleteFile_DeletesWithRetry_CancellationRequested() + public async Task DeleteFile_DeletesWithRetry_CancellationRequested() { using (TestHostContext hc = new TestHostContext(this)) { @@ -1333,5 +1333,50 @@ await processInvoker.ExecuteAsync( cancellationToken: CancellationToken.None); } } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void TryCreateDirectory_CreatesNewDirectory() + { + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Assert.True(IOUtil.TryCreateDirectory(tempPath)); + Assert.True(Directory.Exists(tempPath)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, recursive: true); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void TryCreateDirectory_ReturnsTrueForExistingDirectory() + { + Assert.True(IOUtil.TryCreateDirectory(Path.GetTempPath())); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void TryCreateDirectory_ReturnsFalseOnInvalidPath() + { + // Creating a directory at a path where a file already exists triggers IOException + var tempFile = Path.GetTempFileName(); + try + { + Assert.False(IOUtil.TryCreateDirectory(tempFile)); + } + finally + { + File.Delete(tempFile); + } + } } } diff --git a/src/Test/L0/Worker/Build/GitSourceProviderL0.cs b/src/Test/L0/Worker/Build/GitSourceProviderL0.cs index 000c879f62..b7a2c69eca 100644 --- a/src/Test/L0/Worker/Build/GitSourceProviderL0.cs +++ b/src/Test/L0/Worker/Build/GitSourceProviderL0.cs @@ -174,7 +174,7 @@ public void GetSourceGitClone() [Trait("SkipOn", "darwin")] [Trait("SkipOn", "linux")] [MemberData(nameof(FeatureFlagsStatusData))] - public void TestSetGitConfiguration(bool featureFlagsStatus) + public async Task TestSetGitConfiguration(bool featureFlagsStatus) { var featureFlagStatusString = featureFlagsStatus.ToString(); var invocation = featureFlagsStatus ? Times.Once() : Times.Never(); @@ -202,7 +202,7 @@ public void TestSetGitConfiguration(bool featureFlagsStatus) GitSourceProvider gitSourceProvider = new ExternalGitSourceProvider(); // Act. - gitSourceProvider.SetGitFeatureFlagsConfiguration(executionContext.Object, gitCommandManager.Object, sourceProviderL0Path); + await gitSourceProvider.SetGitFeatureFlagsConfiguration(executionContext.Object, gitCommandManager.Object, sourceProviderL0Path); // Assert. gitCommandManager.Verify(x => x.GitConfig(executionContext.Object, sourceProviderL0Path, "pack.threads", "1"), invocation); diff --git a/src/Test/L0/Worker/ContainerOperationProviderEnhancedL0.cs b/src/Test/L0/Worker/ContainerOperationProviderEnhancedL0.cs new file mode 100644 index 0000000000..e75eb36f08 --- /dev/null +++ b/src/Test/L0/Worker/ContainerOperationProviderEnhancedL0.cs @@ -0,0 +1,319 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Agent.Sdk; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Pipelines = Microsoft.TeamFoundation.DistributedTask.Pipelines; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.VisualStudio.Services.Agent.Tests.Worker +{ + public sealed class ContainerOperationProviderEnhancedL0 : ContainerOperationProviderL0Base + { + // ============================================= + // Legacy path tests (UseEnhancedNodeSelection = false) + // ============================================= + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task StartContainer_WithDockerLabel_SetsNodePath() + { + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabel); + var executionContext = CreateExecutionContextMock(hc); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "node:16" }); + + // Setup IProcessInvoker for non-Windows platforms + if (!PlatformUtil.RunningOnWindows) + { + SetupProcessInvokerMock(hc); + } + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProviderEnhanced(); + provider.Initialize(hc); + + // Act - Call main container code with mocked Docker operations + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + // Assert + Assert.Equal(NodePathFromLabel, container.CustomNodePath); + Assert.Equal(NodePathFromLabel, container.ResultNodePath); + Assert.Contains(NodePathFromLabel, container.ContainerCommand); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [Trait("SkipOn", "windows")] + [Trait("SkipOn", "linux")] + public async Task StartContainer_WithoutDockerLabel_OnMacOS_UsesDefaultNode() + { + // Only run on macOS + if (!PlatformUtil.RunningOnMacOS) + { + return; + } + + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabelEmpty); + var executionContext = CreateExecutionContextMock(hc); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "node:16" }); + + // Setup IProcessInvoker for macOS + SetupProcessInvokerMock(hc); + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProviderEnhanced(); + provider.Initialize(hc); + + // Act - Call main container code with mocked Docker operations + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + // Assert - macOS uses "node" from container + Assert.Equal(DefaultNodeCommand, container.CustomNodePath); + Assert.Equal(DefaultNodeCommand, container.ResultNodePath); + Assert.Contains(DefaultNodeCommand, container.ContainerCommand); + } + } + + // Test 3: Docker label absent - Windows + Linux container only + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [Trait("SkipOn", "darwin")] + [Trait("SkipOn", "linux")] + public async Task StartContainer_WithoutDockerLabel_OnWindowsWithLinuxContainer_UsesDefaultNode() + { + // Only run on Windows + if (!PlatformUtil.RunningOnWindows) + { + return; + } + + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabelEmpty); + var executionContext = CreateExecutionContextMock(hc); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "node:16" }); + // Set container to Linux OS (Windows host running Linux container) + container.ImageOS = PlatformUtil.OS.Linux; + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProviderEnhanced(); + provider.Initialize(hc); + + // Act - Call main container code with mocked Docker operations + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + // Assert - Windows+Linux uses "node" from container + Assert.Equal(DefaultNodeCommand, container.CustomNodePath); + Assert.Equal(DefaultNodeCommand, container.ResultNodePath); + Assert.Contains(DefaultNodeCommand, container.ContainerCommand); + } + } + + // Test 4: Docker label absent - Linux only (uses agent's mounted node from externals) + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [Trait("SkipOn", "darwin")] + [Trait("SkipOn", "windows")] + public async Task StartContainer_WithoutDockerLabel_OnLinux_UsesAgentNode() + { + // Only run on Linux + if (!PlatformUtil.RunningOnLinux) + { + return; + } + + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabelEmpty); + var executionContext = CreateExecutionContextMock(hc); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "node:16" }); + + // Setup IProcessInvoker for Linux + SetupProcessInvokerMock(hc); + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProviderEnhanced(); + provider.Initialize(hc); + + // Act - Call main container code with mocked Docker operations + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + // Assert - Linux uses agent's mounted node + Assert.True(string.IsNullOrEmpty(container.CustomNodePath)); + Assert.NotNull(container.ResultNodePath); + Assert.NotEmpty(container.ResultNodePath); + Assert.Contains(NodeFromAgentExternal, container.ResultNodePath); + Assert.EndsWith("/bin/node", container.ResultNodePath); + Assert.Contains(NodeFromAgentExternal, container.ContainerCommand); + } + } + + // ============================================= + // UseEnhancedNodeSelection path tests + // Knob activated via Moq-specific matcher overrides + // in CreateExecutionContextMock(hc, useNodeVersionStrategy: true). + // No env vars used — avoids parallel test pollution. + // ============================================= + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task StartContainer_UseNodeVersionStrategy_WithDockerLabel_UsesSleepOrPing() + { + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabel); + var executionContext = CreateExecutionContextMock(hc, useNodeVersionStrategy: true); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "node:16" }); + + if (!PlatformUtil.RunningOnWindows) + { + SetupProcessInvokerMock(hc); + } + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProviderEnhanced(); + provider.Initialize(hc); + + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + Assert.Equal(NodePathFromLabel, container.CustomNodePath); + if (PlatformUtil.RunningOnWindows) + { + Assert.Contains("ping -t localhost", container.ContainerCommand); + } + else + { + Assert.Equal("sleep infinity", container.ContainerCommand); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [Trait("SkipOn", "darwin")] + [Trait("SkipOn", "linux")] + public async Task StartContainer_UseNodeVersionStrategy_OnWindows_LinuxContainer_UsesSleepInfinity() + { + if (!PlatformUtil.RunningOnWindows) + { + return; + } + + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabelEmpty); + var executionContext = CreateExecutionContextMock(hc, useNodeVersionStrategy: true); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "ubuntu:22.04" }); + container.ImageOS = PlatformUtil.OS.Linux; + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProviderEnhanced(); + provider.Initialize(hc); + + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + Assert.Equal("sleep infinity", container.ContainerCommand); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [Trait("SkipOn", "darwin")] + [Trait("SkipOn", "linux")] + public async Task StartContainer_UseNodeVersionStrategy_OnWindows_WindowsContainer_UsesPing() + { + if (!PlatformUtil.RunningOnWindows) + { + return; + } + + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabelEmpty); + var executionContext = CreateExecutionContextMock(hc, useNodeVersionStrategy: true); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "myteam-custom-builder:latest" }); + container.ImageOS = PlatformUtil.OS.Windows; + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProviderEnhanced(); + provider.Initialize(hc); + + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + Assert.Contains("ping -t localhost", container.ContainerCommand); + Assert.Contains("nul", container.ContainerCommand); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [Trait("SkipOn", "darwin")] + [Trait("SkipOn", "windows")] + public async Task StartContainer_UseNodeVersionStrategy_OnLinux_UsesSleepInfinity() + { + if (!PlatformUtil.RunningOnLinux) + { + return; + } + + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabelEmpty); + var executionContext = CreateExecutionContextMock(hc, useNodeVersionStrategy: true); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "ubuntu:22.04" }); + + SetupProcessInvokerMock(hc); + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProviderEnhanced(); + provider.Initialize(hc); + + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + Assert.Equal("sleep infinity", container.ContainerCommand); + } + } + } +} diff --git a/src/Test/L0/Worker/ContainerOperationProviderL0.cs b/src/Test/L0/Worker/ContainerOperationProviderL0.cs new file mode 100644 index 0000000000..27b80457fa --- /dev/null +++ b/src/Test/L0/Worker/ContainerOperationProviderL0.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Agent.Sdk; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Pipelines = Microsoft.TeamFoundation.DistributedTask.Pipelines; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.VisualStudio.Services.Agent.Tests.Worker +{ + public sealed class ContainerOperationProviderL0 : ContainerOperationProviderL0Base + { + + [Fact(Skip = "The test is flaky and needs to be fixed using the new container strategy.")] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task StartContainer_WithDockerLabel_SetsNodePath() + { + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabel); + var executionContext = CreateExecutionContextMock(hc); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "node:16" }); + + // Setup IProcessInvoker for non-Windows platforms + if (!PlatformUtil.RunningOnWindows) + { + SetupProcessInvokerMock(hc); + } + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProvider(); + provider.Initialize(hc); + + // Act - Call main container code with mocked Docker operations + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + // Assert + Assert.Equal(NodePathFromLabel, container.CustomNodePath); + Assert.Equal(NodePathFromLabel, container.ResultNodePath); + Assert.Contains(NodePathFromLabel, container.ContainerCommand); + } + } + + [Fact(Skip = "The test is flaky and needs to be fixed using the new container strategy.")] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [Trait("SkipOn", "windows")] + [Trait("SkipOn", "linux")] + public async Task StartContainer_WithoutDockerLabel_OnMacOS_UsesDefaultNode() + { + // Only run on macOS + if (!PlatformUtil.RunningOnMacOS) + { + return; + } + + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabelEmpty); + var executionContext = CreateExecutionContextMock(hc); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "node:16" }); + + // Setup IProcessInvoker for macOS + SetupProcessInvokerMock(hc); + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProvider(); + provider.Initialize(hc); + + typeof(ContainerOperationProvider).GetField("_dockerManger", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.SetValue(provider, dockerManager.Object); + + // Act - Call main container code with mocked Docker operations + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + // Assert - macOS uses "node" from container + Assert.Equal(DefaultNodeCommand, container.CustomNodePath); + Assert.Equal(DefaultNodeCommand, container.ResultNodePath); + Assert.Contains(DefaultNodeCommand, container.ContainerCommand); + } + } + + // Test 3: Docker label absent - Windows + Linux container only + [Fact(Skip = "The test is flaky and needs to be fixed using the new container strategy.")] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [Trait("SkipOn", "darwin")] + [Trait("SkipOn", "linux")] + public async Task StartContainer_WithoutDockerLabel_OnWindowsWithLinuxContainer_UsesDefaultNode() + { + // Only run on Windows + if (!PlatformUtil.RunningOnWindows) + { + return; + } + + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabelEmpty); + var executionContext = CreateExecutionContextMock(hc); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "node:16" }); + // Set container to Linux OS (Windows host running Linux container) + container.ImageOS = PlatformUtil.OS.Linux; + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProvider(); + provider.Initialize(hc); + + typeof(ContainerOperationProvider).GetField("_dockerManger", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.SetValue(provider, dockerManager.Object); + + // Act - Call main container code with mocked Docker operations + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + // Assert - Windows+Linux uses "node" from container + Assert.Equal(DefaultNodeCommand, container.CustomNodePath); + Assert.Equal(DefaultNodeCommand, container.ResultNodePath); + Assert.Contains(DefaultNodeCommand, container.ContainerCommand); + } + } + + // Test 4: Docker label absent - Linux only (uses agent's mounted node from externals) + [Fact(Skip = "The test is flaky and needs to be fixed using the new container strategy.")] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [Trait("SkipOn", "darwin")] + [Trait("SkipOn", "windows")] + public async Task StartContainer_WithoutDockerLabel_OnLinux_UsesAgentNode() + { + // Only run on Linux + if (!PlatformUtil.RunningOnLinux) + { + return; + } + + using (var hc = new TestHostContext(this)) + { + System.IO.Directory.CreateDirectory(hc.GetDirectory(WellKnownDirectory.Work)); + + var dockerManager = CreateDockerManagerMock(NodePathFromLabelEmpty); + var executionContext = CreateExecutionContextMock(hc); + var container = new ContainerInfo(new Pipelines.ContainerResource() { Alias = "test", Image = "node:16" }); + + // Setup IProcessInvoker for Linux + SetupProcessInvokerMock(hc); + + hc.SetSingleton(dockerManager.Object); + + var provider = new ContainerOperationProvider(); + provider.Initialize(hc); + + typeof(ContainerOperationProvider).GetField("_dockerManger", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.SetValue(provider, dockerManager.Object); + + // Act - Call main container code with mocked Docker operations + await provider.StartContainersAsync(executionContext.Object, new List { container }); + + // Assert - Linux uses agent's mounted node + Assert.True(string.IsNullOrEmpty(container.CustomNodePath)); + Assert.NotNull(container.ResultNodePath); + Assert.NotEmpty(container.ResultNodePath); + Assert.Contains(NodeFromAgentExternal, container.ResultNodePath); + Assert.EndsWith("/bin/node", container.ResultNodePath); + Assert.Contains(NodeFromAgentExternal, container.ContainerCommand); + } + } + } +} diff --git a/src/Test/L0/Worker/ContainerOperationProviderL0.md b/src/Test/L0/Worker/ContainerOperationProviderL0.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Test/L0/Worker/ContainerOperationProviderL0Base.cs b/src/Test/L0/Worker/ContainerOperationProviderL0Base.cs new file mode 100644 index 0000000000..2fda430293 --- /dev/null +++ b/src/Test/L0/Worker/ContainerOperationProviderL0Base.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Agent.Sdk; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.Container; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Moq; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Services.Agent; +using Microsoft.TeamFoundation.Framework.Common; + +namespace Microsoft.VisualStudio.Services.Agent.Tests.Worker +{ + public abstract class ContainerOperationProviderL0Base + { + protected const string NodePathFromLabel = "/usr/bin/node"; + protected const string NodePathFromLabelEmpty = ""; + protected const string DefaultNodeCommand = "node"; + protected const string NodeFromAgentExternal = "externals/node"; + + protected Mock CreateDockerManagerMock(string inspectResult) + { + var dockerManager = new Mock(); + dockerManager.Setup(x => x.DockerVersion(It.IsAny())) + .ReturnsAsync(new DockerVersion(new Version("1.35"), new Version("1.35"))); + dockerManager.Setup(x => x.DockerPS(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List { "container123 Up 5 seconds" }); + dockerManager.Setup(x => x.DockerNetworkCreate(It.IsAny(), It.IsAny())) + .ReturnsAsync(0); + dockerManager.Setup(x => x.DockerPull(It.IsAny(), It.IsAny())) + .ReturnsAsync(0); + dockerManager.Setup(x => x.DockerCreate(It.IsAny(), It.IsAny())) + .ReturnsAsync("container123"); + dockerManager.Setup(x => x.DockerStart(It.IsAny(), It.IsAny())) + .ReturnsAsync(0); + dockerManager.Setup(x => x.DockerInspect(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(inspectResult); + dockerManager.Setup(x => x.DockerExec(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync((IExecutionContext context, string containerId, string options, string command, List output) => + { + if (command.Contains("node -v")) + { + output.Add("v16.20.2"); + } + return 0; + }); + return dockerManager; + } + + protected Mock CreateExecutionContextMock(TestHostContext hc, bool useNodeVersionStrategy = false) + { + var executionContext = new Mock(); + var variables = new Variables(hc, new Dictionary(), out var warnings); + executionContext.Setup(x => x.Variables).Returns(variables); + executionContext.Setup(x => x.CancellationToken).Returns(CancellationToken.None); + executionContext.Setup(x => x.GetVariableValueOrDefault(It.IsAny())).Returns(string.Empty); + executionContext.Setup(x => x.Containers).Returns(new List()); + executionContext.Setup(x => x.GetScopedEnvironment()).Returns(new SystemEnvironment()); + + string knobValue = useNodeVersionStrategy ? "true" : "false"; + executionContext.Setup(x => x.GetVariableValueOrDefault("AGENT_USE_ENHANCED_NODE_SELECTION")).Returns(knobValue); + + return executionContext; + } + + protected sealed class FakeProcessInvoker : IProcessInvoker + { + public event EventHandler OutputDataReceived; +#pragma warning disable CS0067 + public event EventHandler ErrorDataReceived; +#pragma warning restore CS0067 + public TimeSpan SigintTimeout { get; set; } + public TimeSpan SigtermTimeout { get; set; } + public bool TryUseGracefulShutdown { get; set; } + + public void Initialize(IHostContext hostContext) { } + + public Task ExecuteAsync(string workingDirectory, string fileName, string arguments, IDictionary environment, CancellationToken cancellationToken) + => ExecuteAsync(workingDirectory, fileName, arguments, environment, false, null, false, cancellationToken); + + public Task ExecuteAsync(string workingDirectory, string fileName, string arguments, IDictionary environment, bool requireExitCodeZero, CancellationToken cancellationToken) + => ExecuteAsync(workingDirectory, fileName, arguments, environment, requireExitCodeZero, null, false, cancellationToken); + + public Task ExecuteAsync(string workingDirectory, string fileName, string arguments, IDictionary environment, bool requireExitCodeZero, Encoding outputEncoding, CancellationToken cancellationToken) + => ExecuteAsync(workingDirectory, fileName, arguments, environment, requireExitCodeZero, outputEncoding, false, cancellationToken); + + public Task ExecuteAsync(string workingDirectory, string fileName, string arguments, IDictionary environment, bool requireExitCodeZero, Encoding outputEncoding, bool killProcessOnCancel, CancellationToken cancellationToken) + => ExecuteAsync(workingDirectory, fileName, arguments, environment, requireExitCodeZero, outputEncoding, killProcessOnCancel, null, cancellationToken); + + public Task ExecuteAsync(string workingDirectory, string fileName, string arguments, IDictionary environment, bool requireExitCodeZero, Encoding outputEncoding, bool killProcessOnCancel, InputQueue redirectStandardIn, CancellationToken cancellationToken) + => ExecuteAsync(workingDirectory, fileName, arguments, environment, requireExitCodeZero, outputEncoding, killProcessOnCancel, redirectStandardIn, false, false, cancellationToken); + + public Task ExecuteAsync(string workingDirectory, string fileName, string arguments, IDictionary environment, bool requireExitCodeZero, Encoding outputEncoding, bool killProcessOnCancel, InputQueue redirectStandardIn, bool inheritConsoleHandler, bool continueAfterCancelProcessTreeKillAttempt, CancellationToken cancellationToken) + => ExecuteAsync(workingDirectory, fileName, arguments, environment, requireExitCodeZero, outputEncoding, killProcessOnCancel, redirectStandardIn, inheritConsoleHandler, false, continueAfterCancelProcessTreeKillAttempt, cancellationToken); + + public Task ExecuteAsync(string workingDirectory, string fileName, string arguments, IDictionary environment, bool requireExitCodeZero, Encoding outputEncoding, bool killProcessOnCancel, InputQueue redirectStandardIn, bool inheritConsoleHandler, bool keepStandardInOpen, bool continueAfterCancelProcessTreeKillAttempt, CancellationToken cancellationToken) + => ExecuteAsync(workingDirectory, fileName, arguments, environment, requireExitCodeZero, outputEncoding, killProcessOnCancel, redirectStandardIn, inheritConsoleHandler, keepStandardInOpen, false, continueAfterCancelProcessTreeKillAttempt, cancellationToken); + + public Task ExecuteAsync(string workingDirectory, string fileName, string arguments, IDictionary environment, bool requireExitCodeZero, Encoding outputEncoding, bool killProcessOnCancel, InputQueue redirectStandardIn, bool inheritConsoleHandler, bool keepStandardInOpen, bool highPriorityProcess, bool continueAfterCancelProcessTreeKillAttempt, CancellationToken cancellationToken) + { + if (fileName == "whoami") + OutputDataReceived?.Invoke(this, new ProcessDataReceivedEventArgs("testuser")); + else if (fileName == "id" && arguments.StartsWith("-u")) + OutputDataReceived?.Invoke(this, new ProcessDataReceivedEventArgs("1000")); + else if (fileName == "id" && arguments.StartsWith("-gn")) + OutputDataReceived?.Invoke(this, new ProcessDataReceivedEventArgs("testgroup")); + else if (fileName == "id" && arguments.StartsWith("-g")) + OutputDataReceived?.Invoke(this, new ProcessDataReceivedEventArgs("1000")); + else if (fileName == "node" && arguments.Contains("-v")) + OutputDataReceived?.Invoke(this, new ProcessDataReceivedEventArgs("v16.20.2")); + + return Task.FromResult(0); + } + + public void Dispose() { } + } + + protected void SetupProcessInvokerMock(TestHostContext hc) + { +#pragma warning disable CA2000 + var processInvoker = new FakeProcessInvoker(); +#pragma warning restore CA2000 + // Enqueue enough instances for all ExecuteCommandAsync calls in container operations + // Each test may call: whoami, id -u, id -g, id -gn, stat, and potentially other commands + // Enqueue 10 instances to ensure we don't run out during test execution + for (int i = 0; i < 10; i++) + { + hc.EnqueueInstance(processInvoker); + } + } + } +} diff --git a/src/Test/L0/Worker/GitManagerL0.cs b/src/Test/L0/Worker/GitManagerL0.cs index 6b057fbff9..4acc657e7f 100644 --- a/src/Test/L0/Worker/GitManagerL0.cs +++ b/src/Test/L0/Worker/GitManagerL0.cs @@ -3,6 +3,7 @@ using System.IO; using System.Threading; +using System.Threading.Tasks; using Microsoft.VisualStudio.Services.Agent.Worker; using Moq; using Xunit; @@ -16,7 +17,7 @@ public sealed class GitManagerL0 [Trait("Category", "Worker")] [Trait("SkipOn", "darwin")] [Trait("SkipOn", "linux")] - public async void DownloadAsync() + public async Task DownloadAsync() { using var tokenSource = new CancellationTokenSource(); using var hostContext = new TestHostContext(this); diff --git a/src/Test/L0/Worker/Handlers/ProcessHandlerL0.cs b/src/Test/L0/Worker/Handlers/ProcessHandlerL0.cs index afa3a8ccb4..de57316154 100644 --- a/src/Test/L0/Worker/Handlers/ProcessHandlerL0.cs +++ b/src/Test/L0/Worker/Handlers/ProcessHandlerL0.cs @@ -14,6 +14,7 @@ using System.Diagnostics; using System; using System.Linq; +using System.Threading.Tasks; namespace Test.L0.Worker.Handlers; @@ -24,7 +25,7 @@ public class ProcessHandlerL0 [Trait("Category", "Worker.Handlers")] [Trait("SkipOn", "linux")] [Trait("SkipOn", "darwin")] - public async void ProcessHandlerV2_BasicExecution() + public async Task ProcessHandlerV2_BasicExecution() { using var hostContext = CreateTestHostContext(); @@ -70,7 +71,7 @@ @echo off [Trait("Category", "Worker.Handlers")] [Trait("SkipOn", "linux")] [Trait("SkipOn", "darwin")] - public async void ProcessHandlerV2_FileExecution() + public async Task ProcessHandlerV2_FileExecution() { using var hostContext = CreateTestHostContext(); @@ -126,7 +127,7 @@ @echo off [Trait("Category", "Worker.Handlers")] [Trait("SkipOn", "linux")] [Trait("SkipOn", "darwin")] - public async void ProcessHandlerV2_Validation_passes() + public async Task ProcessHandlerV2_Validation_passes() { using var hostContext = CreateTestHostContext(); @@ -176,7 +177,7 @@ @echo off [Trait("Category", "Worker.Handlers")] [Trait("SkipOn", "linux")] [Trait("SkipOn", "darwin")] - public async void ProcessHandlerV2_Validation_fails() + public async Task ProcessHandlerV2_Validation_fails() { using var hostContext = CreateTestHostContext(); diff --git a/src/Test/L0/Worker/JobExtensionL0.cs b/src/Test/L0/Worker/JobExtensionL0.cs index d515f056be..c72e031032 100644 --- a/src/Test/L0/Worker/JobExtensionL0.cs +++ b/src/Test/L0/Worker/JobExtensionL0.cs @@ -68,6 +68,9 @@ public override void InitializeJobExtension(IExecutionContext context, IList _asyncCommandContext; private TestHostContext CreateTestContext(CancellationTokenSource _tokenSource, [CallerMemberName] String testName = "") { + // Prevent L0 tests from making a real HTTP download of Node 6 + Environment.SetEnvironmentVariable("AGENT_DISABLE_NODE6_TASKS", "true"); + var hc = new TestHostContext(this, testName); _jobEc = new Agent.Worker.ExecutionContext(); _taskManager = new Mock(); @@ -285,6 +288,9 @@ private TestHostContext CreateTestContext(CancellationTokenSource _tokenSource, private TestHostContext CreateMSITestContext(CancellationTokenSource _tokenSource, [CallerMemberName] String testName = "") { + // Prevent L0 tests from making a real HTTP download of Node 6 + Environment.SetEnvironmentVariable("AGENT_DISABLE_NODE6_TASKS", "true"); + TestHostContext hc = new TestHostContext(this, testName); _jobEc = new Agent.Worker.ExecutionContext(); _taskManager = new Mock(); diff --git a/src/Test/L0/Worker/Release/FetchEngineL0.cs b/src/Test/L0/Worker/Release/FetchEngineL0.cs index 5fd25c8984..fb9d246515 100644 --- a/src/Test/L0/Worker/Release/FetchEngineL0.cs +++ b/src/Test/L0/Worker/Release/FetchEngineL0.cs @@ -51,7 +51,7 @@ public sealed class FetchEngineL0 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void ShouldDownloadAllTheFiles() + public async Task ShouldDownloadAllTheFiles() { var stubContainerProvider = new StubContainerProvider(mockContainerItems, (item1, c) => mockItemContent); using (var fetchEngine = GetFetchEngine(stubContainerProvider, CancellationToken.None)) @@ -67,7 +67,7 @@ public async void ShouldDownloadAllTheFiles() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void ShouldNotDoParallelDownloadIfSpecified() + public async Task ShouldNotDoParallelDownloadIfSpecified() { int concurrentAccessCount = 0; var stubContainerProvider = new StubContainerProvider(mockContainerItems, diff --git a/src/Test/L0/Worker/Release/GitHubArtifactL0.cs b/src/Test/L0/Worker/Release/GitHubArtifactL0.cs index 9bfa611551..e29af2f279 100644 --- a/src/Test/L0/Worker/Release/GitHubArtifactL0.cs +++ b/src/Test/L0/Worker/Release/GitHubArtifactL0.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Tasks; using Microsoft.TeamFoundation.Build.WebApi; using Microsoft.TeamFoundation.DistributedTask.WebApi; @@ -61,7 +62,7 @@ public void MissingEndpointShouldThrowException() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void GitHubArtifactShouldCallGetSourceWithCorrectParameter() + public async Task GitHubArtifactShouldCallGetSourceWithCorrectParameter() { using (TestHostContext tc = Setup()) { diff --git a/src/Test/L0/Worker/Release/JenkinsArtifactL0.cs b/src/Test/L0/Worker/Release/JenkinsArtifactL0.cs index e539515d6a..f7753a00b5 100644 --- a/src/Test/L0/Worker/Release/JenkinsArtifactL0.cs +++ b/src/Test/L0/Worker/Release/JenkinsArtifactL0.cs @@ -29,7 +29,7 @@ public sealed class JenkinsArtifactL0 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void IfNoCommitVersionExistsInArtifactDetailsNoIssueShouldBeAdded() + public async Task IfNoCommitVersionExistsInArtifactDetailsNoIssueShouldBeAdded() { using (TestHostContext tc = Setup()) { @@ -46,7 +46,7 @@ public async void IfNoCommitVersionExistsInArtifactDetailsNoIssueShouldBeAdded() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void ShouldLogAnIssueIfEndVersionIsInvalidInArtifactDetail() + public async Task ShouldLogAnIssueIfEndVersionIsInvalidInArtifactDetail() { using (TestHostContext tc = Setup()) { @@ -65,7 +65,7 @@ public async void ShouldLogAnIssueIfEndVersionIsInvalidInArtifactDetail() [Fact] [TraitAttribute("Level", "L0")] [TraitAttribute("Category", "Worker")] - public async void MissingStartVersionShouldDownloadCommitsFromSingleBuild() + public async Task MissingStartVersionShouldDownloadCommitsFromSingleBuild() { using (TestHostContext tc = Setup()) { @@ -83,7 +83,7 @@ public async void MissingStartVersionShouldDownloadCommitsFromSingleBuild() [Fact] [TraitAttribute("Level", "L0")] [TraitAttribute("Category", "Worker")] - public async void JenkinsCommitsShouldBeFetchedBetweenBuildRange() + public async Task JenkinsCommitsShouldBeFetchedBetweenBuildRange() { using (TestHostContext tc = Setup()) { @@ -105,7 +105,7 @@ public async void JenkinsCommitsShouldBeFetchedBetweenBuildRange() [Fact] [TraitAttribute("Level", "L0")] [TraitAttribute("Category", "Worker")] - public async void JenkinsRollbackCommitsShouldBeFetched() + public async Task JenkinsRollbackCommitsShouldBeFetched() { using (TestHostContext tc = Setup()) { @@ -126,7 +126,7 @@ public async void JenkinsRollbackCommitsShouldBeFetched() [Fact] [TraitAttribute("Level", "L0")] [TraitAttribute("Category", "Worker")] - public async void JenkinsCommitsShouldLogAnIssueIfBuildIsDeleted() + public async Task JenkinsCommitsShouldLogAnIssueIfBuildIsDeleted() { using (TestHostContext tc = Setup()) { @@ -147,7 +147,7 @@ public async void JenkinsCommitsShouldLogAnIssueIfBuildIsDeleted() [Fact] [TraitAttribute("Level", "L0")] [TraitAttribute("Category", "Worker")] - public async void CommitsShouldBeUploadedAsAttachment() + public async Task CommitsShouldBeUploadedAsAttachment() { using (TestHostContext tc = Setup()) { @@ -184,7 +184,7 @@ public async void CommitsShouldBeUploadedAsAttachment() [Fact] [TraitAttribute("Level", "L0")] [TraitAttribute("Category", "Worker")] - public async void CommitsShoulHaveUrlIfItsGitRepo() + public async Task CommitsShoulHaveUrlIfItsGitRepo() { using (TestHostContext tc = Setup()) { diff --git a/src/Test/L0/Worker/Release/TfsGitArtifactL0.cs b/src/Test/L0/Worker/Release/TfsGitArtifactL0.cs index 7915e8ad63..b60d4fa0f3 100644 --- a/src/Test/L0/Worker/Release/TfsGitArtifactL0.cs +++ b/src/Test/L0/Worker/Release/TfsGitArtifactL0.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Tasks; using Microsoft.TeamFoundation.Build.WebApi; using Microsoft.TeamFoundation.DistributedTask.WebApi; using Microsoft.VisualStudio.Services.Agent.Worker; @@ -59,7 +60,7 @@ public void ShouldThrowIfEndpointsDoNotContainTfsGitEndpoint() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void TfsGitArtifactShouldCallGetSourceWithCorrectParameter() + public async Task TfsGitArtifactShouldCallGetSourceWithCorrectParameter() { using (TestHostContext tc = Setup()) { diff --git a/src/Test/L0/Worker/Release/TfsVCArtifactL0.cs b/src/Test/L0/Worker/Release/TfsVCArtifactL0.cs index d78fef715c..695d02e130 100644 --- a/src/Test/L0/Worker/Release/TfsVCArtifactL0.cs +++ b/src/Test/L0/Worker/Release/TfsVCArtifactL0.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Tasks; using Microsoft.TeamFoundation.Build.WebApi; using Microsoft.TeamFoundation.DistributedTask.WebApi; using Microsoft.VisualStudio.Services.Agent.Worker; @@ -62,7 +63,7 @@ public void MissingEndpointShouldThrowException() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void TfsVCArtifactShouldCallGetSourceWithCorrectParameter() + public async Task TfsVCArtifactShouldCallGetSourceWithCorrectParameter() { using (TestHostContext tc = Setup()) { diff --git a/src/Test/L0/Worker/TaskManagerL0.cs b/src/Test/L0/Worker/TaskManagerL0.cs index 0cf81dd9cc..4eccaf572c 100644 --- a/src/Test/L0/Worker/TaskManagerL0.cs +++ b/src/Test/L0/Worker/TaskManagerL0.cs @@ -30,7 +30,7 @@ public sealed class TaskManagerL0 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void BubblesCancellation() + public async Task BubblesCancellation() { try { @@ -105,7 +105,7 @@ public async void BubblesCancellation() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void RetryNetworkException() + public async Task RetryNetworkException() { try { @@ -170,7 +170,7 @@ public async void RetryNetworkException() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void RetryStreamException() + public async Task RetryStreamException() { try { @@ -283,7 +283,7 @@ public void DeserializesPlatformSupportedHandlersOnly() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void DownloadsTasks() + public async Task DownloadsTasks() { try { @@ -356,17 +356,17 @@ public async void DownloadsTasks() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public void PreservesTaskZipTaskWhenInSignatureVerification() + public async Task PreservesTaskZipTaskWhenInSignatureVerification() { - PreservesTaskZipTask(signatureVerification: true); + await PreservesTaskZipTask(signatureVerification: true); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public void PreservesTaskZipTaskWhenAlwaysExtractTask() + public async Task PreservesTaskZipTaskWhenAlwaysExtractTask() { - PreservesTaskZipTask(alwaysExtractTask: true); + await PreservesTaskZipTask(alwaysExtractTask: true); } // TODO: Add test for Extract @@ -792,7 +792,7 @@ private void Teardown() } } - private async void PreservesTaskZipTask(bool signatureVerification = false, bool alwaysExtractTask = false) + private async Task PreservesTaskZipTask(bool signatureVerification = false, bool alwaysExtractTask = false) { try { diff --git a/src/Test/L0/Worker/TestResults/ResultsCommandExtensionTests.cs b/src/Test/L0/Worker/TestResults/ResultsCommandExtensionTests.cs index dc2e01f222..50e6a79e5f 100644 --- a/src/Test/L0/Worker/TestResults/ResultsCommandExtensionTests.cs +++ b/src/Test/L0/Worker/TestResults/ResultsCommandExtensionTests.cs @@ -31,6 +31,7 @@ public sealed class ResultsCommandTests private Mock _mockCustomerIntelligenceServer; private Mock _mockFeatureFlagService; private Variables _variables; + private TestDataPublisher _publisher; public ResultsCommandTests() { @@ -47,6 +48,7 @@ public ResultsCommandTests() _mockFeatureFlagService = new Mock(); _mockFeatureFlagService.Setup(x => x.GetFeatureFlagState(It.IsAny(), It.IsAny())).Returns(true); + _publisher = new TestDataPublisher(); } [Fact] @@ -198,5 +200,285 @@ private TestHostContext SetupMocks([CallerMemberName] string name = "", bool inc return _hc; } + + #region Helper methods + + /// + /// Creates a TestRunData with the given run ID, start date, and test results. + /// + private static TestRunData CreateTestRunData( + string runId, + DateTime startDate, + params (string automatedTestName, string testCaseTitle, string outcome)[] results) + { + var testRunData = new TestRunData(new RunCreateModel(runId ?? "Run")) + { + TestRunIdFromAttachmentFile = runId, + TestRunStartDate = startDate, + TestResults = new List() + }; + + foreach (var (automatedTestName, testCaseTitle, outcome) in results) + { + testRunData.TestResults.Add(new TestCaseResultData + { + AutomatedTestName = automatedTestName, + TestCaseTitle = testCaseTitle, + Outcome = outcome + }); + } + + return testRunData; + } + + #endregion + + #region DetectAndSetRetriesForTestRun tests + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void DetectAndSetRetries_NullList_DoesNotThrow() + { + // Act & Assert – should not throw + _publisher.DetectAndSetRetriesForTestRun(null); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void DetectAndSetRetries_EmptyList_DoesNotThrow() + { + var list = new List(); + + _publisher.DetectAndSetRetriesForTestRun(list); + + Assert.Empty(list); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void DetectAndSetRetries_TwoRunsDifferentIds_NoGrouping() + { + var run1 = CreateTestRunData("runA", DateTime.UtcNow, ("Test1", "Title1", "Passed")); + var run2 = CreateTestRunData("runB", DateTime.UtcNow, ("Test2", "Title2", "Failed")); + var list = new List { run1, run2 }; + + _publisher.DetectAndSetRetriesForTestRun(list); + + Assert.Equal(2, list.Count); + Assert.Null(run1.Retries); + Assert.Null(run2.Retries); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void DetectAndSetRetries_MultipleGroups_GroupedCorrectly() + { + var dateA1 = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); + var dateA2 = new DateTime(2025, 1, 1, 11, 0, 0, DateTimeKind.Utc); + var dateB1 = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); + var dateB2 = new DateTime(2025, 1, 1, 12, 0, 0, DateTimeKind.Utc); + + var runA1 = CreateTestRunData("groupA", dateA1, ("TestA", "TitleA", "Failed")); + var runA2 = CreateTestRunData("groupA", dateA2, ("TestA", "TitleA", "Passed")); + var runB1 = CreateTestRunData("groupB", dateB1, ("TestB", "TitleB", "Failed")); + var runB2 = CreateTestRunData("groupB", dateB2, ("TestB", "TitleB", "Passed")); + + var list = new List { runA2, runB2, runA1, runB1 }; + + _publisher.DetectAndSetRetriesForTestRun(list); + + // Only primary runs remain + Assert.Equal(2, list.Count); + Assert.Contains(runA1, list); + Assert.Contains(runB1, list); + + Assert.Single(runA1.Retries); + Assert.Same(runA2, runA1.Retries[0]); + + Assert.Single(runB1.Retries); + Assert.Same(runB2, runB1.Retries[0]); + } + + #endregion + + #region ProcessTestResults tests + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void ProcessTestResults_NullTestRunData_DoesNotThrow() + { + var dict = new Dictionary(); + TestDataPublisher.ProcessTestResults(null, dict); + Assert.Empty(dict); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void ProcessTestResults_PopulatesDictionary_WithTestNameAsKey() + { + var run = CreateTestRunData(null, DateTime.UtcNow, + ("Namespace.TestA", "Test A Title", "Passed"), + ("Namespace.TestB", "Test B Title", "Failed")); + + var dict = new Dictionary(); + TestDataPublisher.ProcessTestResults(run, dict); + + Assert.Equal(2, dict.Count); + Assert.Equal(TestOutcome.Passed, dict["Namespace.TestA Test A Title"]); + Assert.Equal(TestOutcome.Failed, dict["Namespace.TestB Test B Title"]); + } + + #endregion + + #region GetLatestAttemptResults tests + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void GetLatestAttemptResults_MultipleRetries_LastRetryWins() + { + var primaryRun = CreateTestRunData("run1", DateTime.UtcNow, + ("Test1", "Title1", "Failed")); + + var retry1 = CreateTestRunData("run1", DateTime.UtcNow, + ("Test1", "Title1", "Failed")); // still failing + var retry2 = CreateTestRunData("run1", DateTime.UtcNow, + ("Test1", "Title1", "Passed")); // finally passes + + primaryRun.Retries = new List { retry1, retry2 }; + + var results = _publisher.GetLatestAttemptResults(primaryRun); + + Assert.Single(results); + Assert.Equal(TestOutcome.Passed, results["Test1 Title1"]); + } + + #endregion + + #region GetTestRunOutcomeForRetries tests + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void GetTestRunOutcomeForRetries_NullList_ReturnsFalse() + { + var anyFailed = _publisher.GetTestRunOutcomeForRetries(null, out var summary); + + Assert.False(anyFailed); + Assert.Equal(0, summary.Total); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void GetTestRunOutcomeForRetries_AllPassing_ReturnsFalse() + { + var run = CreateTestRunData("run1", DateTime.UtcNow, + ("Test1", "Title1", "Passed"), + ("Test2", "Title2", "Passed")); + + var list = new List { run }; + var anyFailed = _publisher.GetTestRunOutcomeForRetries(list, out var summary); + + Assert.False(anyFailed); + Assert.Equal(2, summary.Total); + Assert.Equal(2, summary.Passed); + Assert.Equal(0, summary.Failed); + Assert.Equal(0, summary.Skipped); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void GetTestRunOutcomeForRetries_FailedTestResolvedByRetry_ReturnsFalse() + { + var primaryRun = CreateTestRunData("run1", DateTime.UtcNow, + ("Test1", "Title1", "Failed"), + ("Test2", "Title2", "Passed")); + + var retry = CreateTestRunData("run1", DateTime.UtcNow, + ("Test1", "Title1", "Passed")); // fixed in retry + + primaryRun.Retries = new List { retry }; + + var list = new List { primaryRun }; + var anyFailed = _publisher.GetTestRunOutcomeForRetries(list, out var summary); + + Assert.False(anyFailed); + Assert.Equal(2, summary.Total); + Assert.Equal(2, summary.Passed); + Assert.Equal(0, summary.Failed); + } + + #endregion + + #region End-to-end: DetectAndSetRetries + GetTestRunOutcomeForRetries + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void EndToEnd_DetectRetries_ThenEvaluateOutcome_FailureResolvedByRetry() + { + var date1 = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); + var date2 = new DateTime(2025, 1, 1, 11, 0, 0, DateTimeKind.Utc); + + var primaryRun = CreateTestRunData("run1", date1, + ("Test1", "Title1", "Failed"), + ("Test2", "Title2", "Passed")); + + var retryRun = CreateTestRunData("run1", date2, + ("Test1", "Title1", "Passed")); // fixed on retry + + var list = new List { primaryRun, retryRun }; + + // Step 1: detect retries + _publisher.DetectAndSetRetriesForTestRun(list); + + Assert.Single(list); + Assert.NotNull(list[0].Retries); + + // Step 2: evaluate outcome + var anyFailed = _publisher.GetTestRunOutcomeForRetries(list, out var summary); + + Assert.False(anyFailed); + Assert.Equal(2, summary.Total); + Assert.Equal(2, summary.Passed); + Assert.Equal(0, summary.Failed); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "TestRetryHelper")] + public void EndToEnd_DetectRetries_ThenEvaluateOutcome_FailurePersistsAfterRetry() + { + var date1 = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); + var date2 = new DateTime(2025, 1, 1, 11, 0, 0, DateTimeKind.Utc); + + var primaryRun = CreateTestRunData("run1", date1, + ("Test1", "Title1", "Failed"), + ("Test2", "Title2", "Passed")); + + var retryRun = CreateTestRunData("run1", date2, + ("Test1", "Title1", "Failed")); // still failing + + var list = new List { primaryRun, retryRun }; + + _publisher.DetectAndSetRetriesForTestRun(list); + + var anyFailed = _publisher.GetTestRunOutcomeForRetries(list, out var summary); + + Assert.True(anyFailed); + Assert.Equal(2, summary.Total); + Assert.Equal(1, summary.Passed); + Assert.Equal(1, summary.Failed); + } + + #endregion } } diff --git a/src/Test/L0/Worker/WorkerL0.cs b/src/Test/L0/Worker/WorkerL0.cs index a343eab422..133d386970 100644 --- a/src/Test/L0/Worker/WorkerL0.cs +++ b/src/Test/L0/Worker/WorkerL0.cs @@ -69,7 +69,7 @@ private JobCancelMessage CreateJobCancelMessage(Guid jobId) [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void DispatchRunNewJob() + public async Task DispatchRunNewJob() { //Arrange using (var hc = new TestHostContext(this)) @@ -123,7 +123,7 @@ public async void DispatchRunNewJob() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void DispatchCancellation() + public async Task DispatchCancellation() { //Arrange using (var hc = new TestHostContext(this)) @@ -382,7 +382,7 @@ private bool IsMessageIdentical(Pipelines.AgentJobRequestMessage source, Pipelin [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void FlushLogsRequest_WhenFeatureEnabled_TriggersWorkerTimeout() + public async Task FlushLogsRequest_WhenFeatureEnabled_TriggersWorkerTimeout() { // Arrange using (var hc = new TestHostContext(this)) @@ -472,7 +472,7 @@ public async void FlushLogsRequest_WhenFeatureEnabled_TriggersWorkerTimeout() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void FlushLogsRequest_WhenFeatureDisabled_IgnoresRequest() + public async Task FlushLogsRequest_WhenFeatureDisabled_IgnoresRequest() { // Arrange using (var hc = new TestHostContext(this)) @@ -562,7 +562,7 @@ public async void FlushLogsRequest_WhenFeatureDisabled_IgnoresRequest() [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public async void FlushLogsRequest_WhenFeatureNotSet_DefaultsToDisabled() + public async Task FlushLogsRequest_WhenFeatureNotSet_DefaultsToDisabled() { // Arrange using (var hc = new TestHostContext(this)) diff --git a/src/Test/Test.csproj b/src/Test/Test.csproj index 855520ce47..8814af8fe6 100644 --- a/src/Test/Test.csproj +++ b/src/Test/Test.csproj @@ -16,20 +16,24 @@ - - - + + + + + build + - + - + - - - + + + diff --git a/src/dev.sh b/src/dev.sh index 73f3e96b2b..dcc03cd664 100755 --- a/src/dev.sh +++ b/src/dev.sh @@ -42,17 +42,18 @@ fi function get_net_version() { local dotnet_versions=" - net6.0-sdk=6.0.424 - net6.0-runtime=6.0.32 + + net8.0-sdk=8.0.418 + net8.0-runtime=8.0.24 - net8.0-sdk=8.0.401 - net8.0-runtime=8.0.8 + net10.0-sdk=10.0.103 + net10.0-runtime=10.0.3 " echo "$dotnet_versions" | grep -o "$1=[^ ]*" | cut -d '=' -f2 } -DOTNET_SDK_VERSION=$(get_net_version "net8.0-sdk") +DOTNET_SDK_VERSION=$(get_net_version "${TARGET_FRAMEWORK}-sdk") DOTNET_RUNTIME_VERSION=$(get_net_version "${TARGET_FRAMEWORK}-runtime") if [[ ($DOTNET_SDK_VERSION == "") || ($DOTNET_RUNTIME_VERSION == "") ]]; then @@ -112,6 +113,53 @@ function restore_sdk_and_runtime() { fi } +function warn_about_newer_versions() { + echo "" + + # Extract major version from TARGET_FRAMEWORK (e.g., net10.0 -> 10.0, net8.0 -> 8.0) + local dotnet_major_version="${TARGET_FRAMEWORK#net}" + + # Use official .NET APIs to get latest versions + local latest_sdk latest_runtime + local sdk_outdated=false + local runtime_outdated=false + + # Get latest SDK version from official .NET feed + latest_sdk=$(curl -s "https://builds.dotnet.microsoft.com/dotnet/Sdk/${dotnet_major_version}/latest.version" 2>/dev/null | tail -n 1 | tr -d '\r\n' || echo "") + if [[ -z "$latest_sdk" ]]; then + # Fallback to backup feed + latest_sdk=$(curl -s "https://ci.dot.net/public/Sdk/${dotnet_major_version}/latest.version" 2>/dev/null | tail -n 1 | tr -d '\r\n' || echo "$DOTNET_SDK_VERSION") + fi + + # Get latest Runtime version from official .NET feed + latest_runtime=$(curl -s "https://builds.dotnet.microsoft.com/dotnet/Runtime/${dotnet_major_version}/latest.version" 2>/dev/null | tail -n 1 | tr -d '\r\n' || echo "") + if [[ -z "$latest_runtime" ]]; then + # Fallback to backup feed + latest_runtime=$(curl -s "https://ci.dot.net/public/Runtime/${dotnet_major_version}/latest.version" 2>/dev/null | tail -n 1 | tr -d '\r\n' || echo "$DOTNET_RUNTIME_VERSION") + fi + + # Check SDK version + if [[ -n "$latest_sdk" && "$latest_sdk" != "$DOTNET_SDK_VERSION" ]]; then + sdk_outdated=true + fi + + # Check Runtime version + if [[ -n "$latest_runtime" && "$latest_runtime" != "$DOTNET_RUNTIME_VERSION" ]]; then + runtime_outdated=true + fi + + if [[ "$sdk_outdated" == "true" || "$runtime_outdated" == "true" ]]; then + echo "⚠️ WARNING: Newer .NET ${dotnet_major_version} versions available:" >&2 + if [[ "$sdk_outdated" == "true" ]]; then + echo " SDK: $latest_sdk (currently using $DOTNET_SDK_VERSION)" >&2 + fi + if [[ "$runtime_outdated" == "true" ]]; then + echo " Runtime: $latest_runtime (currently using $DOTNET_RUNTIME_VERSION)" >&2 + fi + echo " Consider updating versions in dev.sh" >&2 + fi +} + function detect_platform_and_runtime_id() { heading "Platform / RID detection" @@ -222,7 +270,7 @@ function cmd_test_l0() { TestFilters="$TestFilters&$DEV_TEST_FILTERS" fi - dotnet msbuild -t:testl0 -p:PackageRuntime="${RUNTIME_ID}" -p:PackageType="${PACKAGE_TYPE}" -p:BUILDCONFIG="${BUILD_CONFIG}" -p:AgentVersion="${AGENT_VERSION}" -p:LayoutRoot="${LAYOUT_DIR}" -p:TestFilters="${TestFilters}" -p:TargetFramework="${TARGET_FRAMEWORK}" -p:RuntimeFrameworkVersion="${DOTNET_RUNTIME_VERSION}" "${DEV_ARGS[@]}" || failed "failed tests" + dotnet msbuild -t:testl0 -tl:off -p:PackageRuntime="${RUNTIME_ID}" -p:PackageType="${PACKAGE_TYPE}" -p:BUILDCONFIG="${BUILD_CONFIG}" -p:AgentVersion="${AGENT_VERSION}" -p:LayoutRoot="${LAYOUT_DIR}" -p:TestFilters="${TestFilters}" -p:TargetFramework="${TARGET_FRAMEWORK}" -p:RuntimeFrameworkVersion="${DOTNET_RUNTIME_VERSION}" "${DEV_ARGS[@]}" || failed "failed tests" } function cmd_test_l1() { @@ -244,7 +292,7 @@ function cmd_test_l1() { TestFilters="$TestFilters&$DEV_TEST_FILTERS" fi - dotnet msbuild -t:testl1 -p:PackageRuntime="${RUNTIME_ID}" -p:PackageType="${PACKAGE_TYPE}" -p:BUILDCONFIG="${BUILD_CONFIG}" -p:AgentVersion="${AGENT_VERSION}" -p:LayoutRoot="${LAYOUT_DIR}" -p:TestFilters="${TestFilters}" -p:TargetFramework="${TARGET_FRAMEWORK}" -p:RuntimeFrameworkVersion="${DOTNET_RUNTIME_VERSION}" "${DEV_ARGS[@]}" || failed "failed tests" + dotnet msbuild -t:testl1 -tl:off -p:PackageRuntime="${RUNTIME_ID}" -p:PackageType="${PACKAGE_TYPE}" -p:BUILDCONFIG="${BUILD_CONFIG}" -p:AgentVersion="${AGENT_VERSION}" -p:LayoutRoot="${LAYOUT_DIR}" -p:TestFilters="${TestFilters}" -p:TargetFramework="${TARGET_FRAMEWORK}" -p:RuntimeFrameworkVersion="${DOTNET_RUNTIME_VERSION}" "${DEV_ARGS[@]}" || failed "failed tests" } function cmd_test() { @@ -342,12 +390,7 @@ function cmd_report() { echo "Found coverage file $LATEST_COVERAGE_FILE" COVERAGE_XML_FILE="$COVERAGE_REPORT_DIR/coverage.xml" echo "Converting to XML file $COVERAGE_XML_FILE" - - # for some reason CodeCoverage.exe will only write the output file in the current directory - pushd $COVERAGE_REPORT_DIR >/dev/null - "${HOME}/.nuget/packages/microsoft.codecoverage/16.4.0/build/netstandard1.0/CodeCoverage/CodeCoverage.exe" analyze "/output:coverage.xml" "$LATEST_COVERAGE_FILE" - popd >/dev/null - + dotnet-coverage merge "$LATEST_COVERAGE_FILE" --output "$COVERAGE_XML_FILE" --output-format xml if ! command -v reportgenerator.exe >/dev/null; then echo "reportgenerator not installed. Skipping generation of HTML reports" echo "To install: " @@ -439,9 +482,10 @@ REPORT_DIR="${REPO_ROOT}/_reports/${RUNTIME_ID}" restore_dotnet_install_script restore_sdk_and_runtime + heading ".NET SDK to path" echo "Adding .NET SDK to PATH (${DOTNET_DIR})" -export PATH=${DOTNET_DIR}:$PATH +export PATH=${DOTNET_DIR}/sdk/${DOTNET_SDK_VERSION}:${DOTNET_DIR}:$PATH export PATH=${NUGET_DIR}:$PATH echo "Path = $PATH" echo ".NET Version = $(dotnet --version)" @@ -487,6 +531,10 @@ case $DEV_CMD in esac popd + +# Check for newer .NET versions at the end so it's visible +warn_about_newer_versions + echo echo Done. echo diff --git a/src/dir.proj b/src/dir.proj index d4368522f4..a70d81baf0 100644 --- a/src/dir.proj +++ b/src/dir.proj @@ -66,21 +66,22 @@ BuildInParallel="false" Projects="@(ProjectFiles)" SkipNonExistentProjects="false" - StopOnFirstFailure="true" /> + StopOnFirstFailure="true" + Properties="NetTargetFramework=$(TargetFramework);NetRuntimeFrameworkVersion=$(RuntimeFrameworkVersion)" /> + Properties="Configuration=$(BUILDCONFIG);PackageRuntime=$(PackageRuntime);Version=$(AgentVersion);RuntimeIdentifier=$(PackageRuntime);PublishDir=$(LayoutRoot)/bin;TreatWarningsAsErrors=$(TreatWarningsAsErrors);NetTargetFramework=$(TargetFramework);NetRuntimeFrameworkVersion=$(RuntimeFrameworkVersion)" /> - - - + + + @@ -91,18 +92,19 @@ BuildInParallel="false" Projects="@(ProjectFiles)" SkipNonExistentProjects="false" - StopOnFirstFailure="true" /> + StopOnFirstFailure="true" + Properties="NetTargetFramework=$(TargetFramework);NetRuntimeFrameworkVersion=$(RuntimeFrameworkVersion)" /> + Properties="Configuration=$(BUILDCONFIG);PackageRuntime=$(PackageRuntime);Version=1.0.0.0;RuntimeIdentifier=$(PackageRuntime);PublishDir=$(L1Root)/bin;NetTargetFramework=$(TargetFramework);NetRuntimeFrameworkVersion=$(RuntimeFrameworkVersion)" /> - - - + + +