From 6a4e43cebdecb716b377a17152569e1ccc79b832 Mon Sep 17 00:00:00 2001 From: "Pierson Lee (PIE)" Date: Mon, 13 Oct 2025 15:32:53 -0700 Subject: [PATCH 01/48] Split the VS PR stage separately from the upload steps There is sometimes an issue with the clone/create PR step. When that happens, the stage isn't requeuable because the VSTS upload step fails when its run again because the payload is already there. This update allows the step to clone the VS repo and create the PR to be rerun due to issues independent of the Upload step. --- azure-pipelines/vs-insertion.yml | 24 +++++++++++++++++++++--- azure-pipelines/vs-validation.yml | 29 +++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index c19331d..23e8a05 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -27,10 +27,10 @@ extends: sourceAnalysisPool: VSEngSS-MicroBuild2022-1ES stages: - - stage: insertion + - stage: upload_payload jobs: - - job: insertion - displayName: VS insertion + - job: upload_payload + displayName: Upload VS payload pool: VSEngSS-MicroBuild2022-1ES templateContext: outputParentDirectory: $(Pipeline.Workspace)/CI @@ -56,6 +56,24 @@ extends: packageParentPath: $(Pipeline.Workspace)/CI/VSInsertion-Windows allowPackageConflicts: true publishVstsFeed: VS + + - stage: insertion + dependsOn: upload_payload + condition: succeeded('upload_payload') + jobs: + - job: insertion + displayName: VS insertion + pool: VSEngSS-MicroBuild2022-1ES + templateContext: + outputParentDirectory: $(Pipeline.Workspace)/CI + steps: + - checkout: none + - powershell: Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)" + displayName: โš™๏ธ Set pipeline name + - template: azure-pipelines/release-deployment-prep.yml@self + - download: CI + artifact: VSInsertion-Windows + displayName: ๐Ÿ”ป Download VSInsertion-Windows artifact - task: MicroBuildInsertVsPayload@5 displayName: ๐Ÿญ Insert VS Payload inputs: diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml index b9d46b7..42d1dab 100644 --- a/azure-pipelines/vs-validation.yml +++ b/azure-pipelines/vs-validation.yml @@ -63,11 +63,11 @@ extends: ArchiveSymbols: false RealSign: false - - stage: insertion - displayName: VS insertion + - stage: upload_payload + displayName: Upload VS payload jobs: - - job: insertion - displayName: VS insertion + - job: upload_payload + displayName: Upload VS payload pool: VSEngSS-MicroBuild2022-1ES steps: - checkout: self @@ -95,6 +95,27 @@ extends: packageParentPath: $(Pipeline.Workspace)/VSInsertion-Windows allowPackageConflicts: true publishVstsFeed: VS + + - stage: insertion + displayName: VS insertion + dependsOn: upload_payload + condition: succeeded('upload_payload') + jobs: + - job: insertion + displayName: VS insertion + pool: VSEngSS-MicroBuild2022-1ES + steps: + - checkout: self + clean: true + fetchDepth: 1 + - download: current + artifact: Variables-Windows + displayName: ๐Ÿ”ป Download Variables-Windows artifact + - powershell: $(Pipeline.Workspace)/Variables-Windows/_define.ps1 + displayName: โš™๏ธ Set pipeline variables based on artifacts + - download: current + artifact: VSInsertion-Windows + displayName: ๐Ÿ”ป Download VSInsertion-Windows artifact - task: MicroBuildInsertVsPayload@5 displayName: ๐Ÿญ Insert VS Payload inputs: From 9ca01b2dcd7574b32b5e7b1572916e544450e247 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 16 Oct 2025 17:07:41 -0600 Subject: [PATCH 02/48] Split into jobs instead of stages Also remove a task that was duplicated (I think) unnecessarily. --- azure-pipelines/vs-insertion.yml | 16 ++++------------ azure-pipelines/vs-validation.yml | 15 ++++----------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index 23e8a05..ba9b521 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -27,9 +27,9 @@ extends: sourceAnalysisPool: VSEngSS-MicroBuild2022-1ES stages: - - stage: upload_payload + - stage: insertion jobs: - - job: upload_payload + - job: upload displayName: Upload VS payload pool: VSEngSS-MicroBuild2022-1ES templateContext: @@ -57,23 +57,15 @@ extends: allowPackageConflicts: true publishVstsFeed: VS - - stage: insertion - dependsOn: upload_payload - condition: succeeded('upload_payload') - jobs: - - job: insertion + - job: insert + dependsOn: upload displayName: VS insertion pool: VSEngSS-MicroBuild2022-1ES templateContext: outputParentDirectory: $(Pipeline.Workspace)/CI steps: - checkout: none - - powershell: Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)" - displayName: โš™๏ธ Set pipeline name - template: azure-pipelines/release-deployment-prep.yml@self - - download: CI - artifact: VSInsertion-Windows - displayName: ๐Ÿ”ป Download VSInsertion-Windows artifact - task: MicroBuildInsertVsPayload@5 displayName: ๐Ÿญ Insert VS Payload inputs: diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml index 42d1dab..4319884 100644 --- a/azure-pipelines/vs-validation.yml +++ b/azure-pipelines/vs-validation.yml @@ -63,10 +63,10 @@ extends: ArchiveSymbols: false RealSign: false - - stage: upload_payload - displayName: Upload VS payload + - stage: insertion + displayName: VS insertion jobs: - - job: upload_payload + - job: upload displayName: Upload VS payload pool: VSEngSS-MicroBuild2022-1ES steps: @@ -96,13 +96,9 @@ extends: allowPackageConflicts: true publishVstsFeed: VS - - stage: insertion - displayName: VS insertion - dependsOn: upload_payload - condition: succeeded('upload_payload') - jobs: - job: insertion displayName: VS insertion + dependsOn: upload pool: VSEngSS-MicroBuild2022-1ES steps: - checkout: self @@ -113,9 +109,6 @@ extends: displayName: ๐Ÿ”ป Download Variables-Windows artifact - powershell: $(Pipeline.Workspace)/Variables-Windows/_define.ps1 displayName: โš™๏ธ Set pipeline variables based on artifacts - - download: current - artifact: VSInsertion-Windows - displayName: ๐Ÿ”ป Download VSInsertion-Windows artifact - task: MicroBuildInsertVsPayload@5 displayName: ๐Ÿญ Insert VS Payload inputs: From 14a7d275d59780ea380bfbb9992baaefeb918c83 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 2 Jun 2026 08:55:44 -0600 Subject: [PATCH 03/48] Call out `dotnet-test-cloud.ps1` script --- CONTRIBUTING.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4322a7..05e4d16 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,11 @@ This repository can be built on Windows, Linux, and OSX. Building, testing, and packing this repository can be done by using the standard dotnet CLI commands (e.g. `dotnet build`, `dotnet test`, `dotnet pack`, etc.). -[pwsh]: https://learn.microsoft.com/powershell/scripting/install/installing-powershell +## Testing + +You can use `dotnet test` to build and/or test the repo. + +There may be tests that are known to be unstable or have special requirements. These can be avoided by running tests using the [dotnet-test-cloud.ps1](tools/dotnet-test-cloud.ps1) script *after* running `dotnet build`. ## Releases @@ -96,3 +100,5 @@ git checkout origin/main # resolve any conflicts, then commit the merge commit. git push origin -u HEAD ``` + +[pwsh]: https://learn.microsoft.com/powershell/scripting/install/installing-powershell From 8be6654ffab1359c69821619911bda4f072e165d Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 2 Jun 2026 11:24:36 -0600 Subject: [PATCH 04/48] Add Copilot skill for bundling dependency update PRs --- .github/skills/bundle-dependency-prs/SKILL.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/skills/bundle-dependency-prs/SKILL.md diff --git a/.github/skills/bundle-dependency-prs/SKILL.md b/.github/skills/bundle-dependency-prs/SKILL.md new file mode 100644 index 0000000..50f8b55 --- /dev/null +++ b/.github/skills/bundle-dependency-prs/SKILL.md @@ -0,0 +1,42 @@ +--- +name: bundle-dependency-prs +description: Fix broken dependency update PRs and aggregate the ones that work into one PR. +disable-model-invocation: true +--- + +# Instructions + +You have two goals: + +1. Get all dependency PRs to a state where their PR checks pass. +2. Aggregate dependency PRs with passing checks into just one PR. + +You can identify dependency update PRs by those authored by `dependabot` or `renovate`. + +You'll find instructions for building and validating the repo in the [CONTRIBUTING.md](../../../CONTRIBUTING.md) doc. +Always validate your changes locally before pushing them to the remote repository. + +When writing PR bodies or comments, avoid unmatched markdown code fences. Keep markdown well-formed. + +## Fix up dependency PRs with failing checks + +Before aggregating PRs, first try to fix any individual dependency update PRs with failing build/test checks. + +1. For the dependency PRs with failing build or test PR checks, check out their source branch and fix any issues. +2. Push your fixes as fresh commits to the individual dependency PRs. +3. If you can't fix a particular PR, add a comment to the PR describing your attempt and outcome. + +## Group dependency PRs that are ready to go + +Your next goal is to collect all the dependency updates that are ready to go into a single PR. + +1. Prepare a local branch called `bulkDepUpdates`. + 1. Consider that a remote branch by the same name may already exist. If it does, base your local branch on it. + 2. Merge `origin/main` into this branch. + 3. Resolve any conflicts. +2. For the dependency PRs whose build and test PR checks already pass, merge them into the `bulkDepUpdates` branch. + Consider that your local branch may have already merged an equivalent PR in the past (from a past run). If so, you should skip merging that PR. + Resolve any conflicts. + Build and run tests to validate your branch. +3. Push the branch. +4. Create a PR, if one does not already exist. From e6bb7a16e9aeb35b4e6af6fae116e7051ee96263 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 2 Jun 2026 11:49:47 -0600 Subject: [PATCH 05/48] Call out docfx checks when bundling dependency update PRs --- .github/skills/bundle-dependency-prs/SKILL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/skills/bundle-dependency-prs/SKILL.md b/.github/skills/bundle-dependency-prs/SKILL.md index 50f8b55..a147e81 100644 --- a/.github/skills/bundle-dependency-prs/SKILL.md +++ b/.github/skills/bundle-dependency-prs/SKILL.md @@ -18,6 +18,9 @@ Always validate your changes locally before pushing them to the remote repositor When writing PR bodies or comments, avoid unmatched markdown code fences. Keep markdown well-formed. +For purposes of assessing PR readiness by its PR checks, consider docfx related checks to be irrelevant. +If a docfx check fails but all other checks succeed, then that is a 'successful' dependency update PR. + ## Fix up dependency PRs with failing checks Before aggregating PRs, first try to fix any individual dependency update PRs with failing build/test checks. From 81b34965726bf8bea093822bde1528e9aad32af5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:55:04 -0600 Subject: [PATCH 06/48] Update actions/checkout action to v6.0.3 (523) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/libtemplate-update.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26bc9eb..69e7ae4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: - windows-2025 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: โš™ Install prerequisites @@ -75,7 +75,7 @@ jobs: name: ๐Ÿ“ƒ Docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: ๐Ÿ”— Markup Link Checker (mlc) uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 with: diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 3c032fa..d933871 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -26,7 +26,7 @@ jobs: # You can define any steps you want, and they will run before the agent starts. # If you do not check out your code, Copilot will do this for you. steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: โš™ Install prerequisites diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index fbfcd14..99b3c05 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,7 +24,7 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: โš™ Install prerequisites diff --git a/.github/workflows/libtemplate-update.yml b/.github/workflows/libtemplate-update.yml index 62a6417..fc8a631 100644 --- a/.github/workflows/libtemplate-update.yml +++ b/.github/workflows/libtemplate-update.yml @@ -17,7 +17,7 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. From 2816e2020f300f27f1abf943f6db0e49ed7badb9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:06:04 -0600 Subject: [PATCH 07/48] Update dependency nerdbank.dotnetrepotools to v1.5.6 (524) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index a5037f9..d0e7d7f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -31,7 +31,7 @@ "rollForward": false }, "nerdbank.dotnetrepotools": { - "version": "1.4.1", + "version": "1.5.6", "commands": [ "repo" ], From dba613e0d4a1a94250ea4224450e3cea68c89f60 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:30:25 -0600 Subject: [PATCH 08/48] Update dependency nerdbank.dotnetrepotools to v1.5.15 (525) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d0e7d7f..e11e39c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -31,7 +31,7 @@ "rollForward": false }, "nerdbank.dotnetrepotools": { - "version": "1.5.6", + "version": "1.5.15", "commands": [ "repo" ], From cd79894c0736662230511b03853e4ceff353f5ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:56:58 -0600 Subject: [PATCH 09/48] Update dependency dotnet-coverage to v18.8.0 (526) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index e11e39c..f88bae6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "dotnet-coverage": { - "version": "18.7.0", + "version": "18.8.0", "commands": [ "dotnet-coverage" ], From 03a29b92ffecc6951c3379a3b737db47ae7dfbde Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 03:55:42 +0000 Subject: [PATCH 10/48] Update Dockerfile and global.json updates to v10.0.301 (527) * Update Dockerfile and global.json updates to v10.0.301 * Bump global.json too --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Andrew Arnott --- .devcontainer/Dockerfile | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 810a6ed..08c0185 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions -FROM mcr.microsoft.com/dotnet/sdk:10.0.300@sha256:c0790639332692a0d56cdd81ed581cfd24d040d9839764c138994866df89a3b6 +FROM mcr.microsoft.com/dotnet/sdk:10.0.301@sha256:548d93f8a18a1acbe6cc127bc4f47281430d34a9e35c18afa80a8d6741c2adc3 # Installing mono makes `dotnet test` work without errors even for net472. # But installing it takes a long time, so it's excluded by default. diff --git a/global.json b/global.json index bc8f3b8..4c9fb87 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.300", + "version": "10.0.301", "rollForward": "patch", "allowPrerelease": false }, From d48cdf885d0e3661e442b5b955d3195469f298ff Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 10 Jun 2026 07:20:24 -0600 Subject: [PATCH 11/48] Fix GitHub Actions env file writes (528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use explicit UTF-8 appends for GitHub Actions environment files so Linux runners don't hit an invalid empty-name env entry after build. This updates both the general env propagation helper and the pipeline variable definition script. Co-authored-by: ๐Ÿค– Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/Set-EnvVars.ps1 | 16 ++++++++++++++-- tools/variables/_define.ps1 | 14 +++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/tools/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1 index 2bd3ca4..460e63e 100644 --- a/tools/Set-EnvVars.ps1 +++ b/tools/Set-EnvVars.ps1 @@ -43,6 +43,18 @@ if ($env:GITHUB_ACTIONS) { Write-Host "GitHub Actions detected. Logging commands will be used to propagate environment variables and prepend path." } +function Add-GitHubActionsFileCommand { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + [Parameter(Mandatory = $true)] + [string]$Value + ) + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($Path, "$Value`n", $utf8NoBom) +} + $CmdEnvScript = '' $Variables.GetEnumerator() |% { Set-Item -LiteralPath env:$($_.Key) -Value $_.Value @@ -52,7 +64,7 @@ $Variables.GetEnumerator() |% { Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)" } if ($env:GITHUB_ACTIONS) { - Add-Content -LiteralPath $env:GITHUB_ENV -Value "$($_.Key)=$($_.Value)" + Add-GitHubActionsFileCommand -Path $env:GITHUB_ENV -Value "$($_.Key)=$($_.Value)" } if ($cmdInstructions) { @@ -79,7 +91,7 @@ if ($PrependPath) { Write-Host "##vso[task.prependpath]$_" } if ($env:GITHUB_ACTIONS) { - Add-Content -LiteralPath $env:GITHUB_PATH -Value $_ + Add-GitHubActionsFileCommand -Path $env:GITHUB_PATH -Value $_ } $CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%" diff --git a/tools/variables/_define.ps1 b/tools/variables/_define.ps1 index d40e5cf..8cb536c 100644 --- a/tools/variables/_define.ps1 +++ b/tools/variables/_define.ps1 @@ -11,6 +11,18 @@ param ( ) +function Add-GitHubActionsFileCommand { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + [Parameter(Mandatory = $true)] + [string]$Value + ) + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($Path, "$Value`n", $utf8NoBom) +} + (& "$PSScriptRoot\_all.ps1").GetEnumerator() |% { # Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive. $keyCaps = $_.Key.ToUpper() @@ -24,7 +36,7 @@ param ( # and the second that works across jobs and stages but must be fully qualified when referenced. Write-Host "##vso[task.setvariable variable=$keyCaps;isOutput=true]$($_.Value)" } elseif ($env:GITHUB_ACTIONS) { - Add-Content -LiteralPath $env:GITHUB_ENV -Value "$keyCaps=$($_.Value)" + Add-GitHubActionsFileCommand -Path $env:GITHUB_ENV -Value "$keyCaps=$($_.Value)" } Set-Item -LiteralPath "env:$keyCaps" -Value $_.Value } From 910fad855551262c156477cf9d50d04b54c61a3e Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 10 Jun 2026 10:48:58 -0600 Subject: [PATCH 12/48] Update actions/checkout action to v6.0.3 --- .github/workflows/docs_validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs_validate.yml b/.github/workflows/docs_validate.yml index fa08fdb..96e0632 100644 --- a/.github/workflows/docs_validate.yml +++ b/.github/workflows/docs_validate.yml @@ -13,7 +13,7 @@ jobs: name: ๐Ÿ“š Doc validation runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: ๐Ÿ”— Markup Link Checker (mlc) From 72ac8a906ecacc231b03fe992280f97ad9029ab6 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 10 Jun 2026 12:02:31 -0600 Subject: [PATCH 13/48] Improve PR checks after fixing dependency PRs --- .github/skills/bundle-dependency-prs/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/skills/bundle-dependency-prs/SKILL.md b/.github/skills/bundle-dependency-prs/SKILL.md index a147e81..48dedbe 100644 --- a/.github/skills/bundle-dependency-prs/SKILL.md +++ b/.github/skills/bundle-dependency-prs/SKILL.md @@ -27,6 +27,7 @@ Before aggregating PRs, first try to fix any individual dependency update PRs wi 1. For the dependency PRs with failing build or test PR checks, check out their source branch and fix any issues. 2. Push your fixes as fresh commits to the individual dependency PRs. + If pushing to a repo in the `microsoft` org when the PR is authord by `renovate`, follow-up the push with a PR comment that says exactly this: "/azp run" which triggers PR checks to re-run. 3. If you can't fix a particular PR, add a comment to the PR describing your attempt and outcome. ## Group dependency PRs that are ready to go From 937aeb05c332bdc78f26e6420a7bd4438182ab83 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 11 Jun 2026 08:12:59 -0600 Subject: [PATCH 14/48] Update Microsoft Testing Platform to 18.8.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a10e354..8630c45 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + From 8ce74d4fc06c1923546ca84e8431b83a362f86d8 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 15 Jun 2026 08:32:30 -0600 Subject: [PATCH 15/48] Support multi-line env var values properly --- tools/Set-EnvVars.ps1 | 24 ++++++++++++++++-------- tools/variables/_define.ps1 | 14 +++++++++++--- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/tools/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1 index 460e63e..6fa4ffb 100644 --- a/tools/Set-EnvVars.ps1 +++ b/tools/Set-EnvVars.ps1 @@ -43,16 +43,24 @@ if ($env:GITHUB_ACTIONS) { Write-Host "GitHub Actions detected. Logging commands will be used to propagate environment variables and prepend path." } -function Add-GitHubActionsFileCommand { +function Add-GitHubActionsEnvVariable { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] - [string]$Value - ) - - $utf8NoBom = [System.Text.UTF8Encoding]::new($false) - [System.IO.File]::AppendAllText($Path, "$Value`n", $utf8NoBom) + [string]$Name, + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Name)) { + throw "GitHub Actions environment variable names must not be empty." + } + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + $delimiter = [guid]::NewGuid().ToString('N') + [System.IO.File]::AppendAllText($Path, "$Name<<$delimiter`n$Value`n$delimiter`n", $utf8NoBom) } $CmdEnvScript = '' @@ -64,7 +72,7 @@ $Variables.GetEnumerator() |% { Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)" } if ($env:GITHUB_ACTIONS) { - Add-GitHubActionsFileCommand -Path $env:GITHUB_ENV -Value "$($_.Key)=$($_.Value)" + Add-GitHubActionsEnvVariable -Path $env:GITHUB_ENV -Name $_.Key -Value ([string]$_.Value) } if ($cmdInstructions) { @@ -91,7 +99,7 @@ if ($PrependPath) { Write-Host "##vso[task.prependpath]$_" } if ($env:GITHUB_ACTIONS) { - Add-GitHubActionsFileCommand -Path $env:GITHUB_PATH -Value $_ + Add-GitHubActionsEnvVariable -Path $env:GITHUB_ENV -Name PATH -Value $newPathValue } $CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%" diff --git a/tools/variables/_define.ps1 b/tools/variables/_define.ps1 index 8cb536c..4a8503d 100644 --- a/tools/variables/_define.ps1 +++ b/tools/variables/_define.ps1 @@ -11,16 +11,24 @@ param ( ) -function Add-GitHubActionsFileCommand { +function Add-GitHubActionsEnvVariable { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [AllowEmptyString()] [string]$Value ) + if ([string]::IsNullOrWhiteSpace($Name)) { + throw "GitHub Actions environment variable names must not be empty." + } + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) - [System.IO.File]::AppendAllText($Path, "$Value`n", $utf8NoBom) + $delimiter = [guid]::NewGuid().ToString('N') + [System.IO.File]::AppendAllText($Path, "$Name<<$delimiter`n$Value`n$delimiter`n", $utf8NoBom) } (& "$PSScriptRoot\_all.ps1").GetEnumerator() |% { @@ -36,7 +44,7 @@ function Add-GitHubActionsFileCommand { # and the second that works across jobs and stages but must be fully qualified when referenced. Write-Host "##vso[task.setvariable variable=$keyCaps;isOutput=true]$($_.Value)" } elseif ($env:GITHUB_ACTIONS) { - Add-GitHubActionsFileCommand -Path $env:GITHUB_ENV -Value "$keyCaps=$($_.Value)" + Add-GitHubActionsEnvVariable -Path $env:GITHUB_ENV -Name $keyCaps -Value ([string]$_.Value) } Set-Item -LiteralPath "env:$keyCaps" -Value $_.Value } From 22ebcf56330c216c8b39d6b746b05b3ebda78c58 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 15 Jun 2026 08:36:28 -0600 Subject: [PATCH 16/48] Centralize the github env var function --- tools/GitHubActions.ps1 | 18 ++++++++++++++++++ tools/Set-EnvVars.ps1 | 37 ++++++++++--------------------------- tools/variables/_define.ps1 | 22 ++-------------------- 3 files changed, 30 insertions(+), 47 deletions(-) create mode 100644 tools/GitHubActions.ps1 diff --git a/tools/GitHubActions.ps1 b/tools/GitHubActions.ps1 new file mode 100644 index 0000000..c742a58 --- /dev/null +++ b/tools/GitHubActions.ps1 @@ -0,0 +1,18 @@ +function Add-GitHubActionsEnvVariable { + param( + [string]$Path = $env:GITHUB_ENV, + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Name)) { + throw "GitHub Actions environment variable names must not be empty." + } + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + $delimiter = [guid]::NewGuid().ToString('N') + [System.IO.File]::AppendAllText($Path, "$Name<<$delimiter`n$Value`n$delimiter`n", $utf8NoBom) +} diff --git a/tools/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1 index 6fa4ffb..0816c0e 100644 --- a/tools/Set-EnvVars.ps1 +++ b/tools/Set-EnvVars.ps1 @@ -12,13 +12,15 @@ The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment. This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process. #> -[CmdletBinding(SupportsShouldProcess=$true)] +[CmdletBinding(SupportsShouldProcess = $true)] Param( - [Parameter(Mandatory=$true, Position=1)] + [Parameter(Mandatory = $true, Position = 1)] $Variables, [string[]]$PrependPath ) +. "$PSScriptRoot\GitHubActions.ps1" + if ($Variables.Count -eq 0) { return $true } @@ -27,7 +29,8 @@ $cmdInstructions = !$env:TF_BUILD -and !$env:GITHUB_ACTIONS -and !$env:CmdEnvScr if ($cmdInstructions) { Write-Warning "Environment variables have been set that will be lost because you're running under cmd.exe" Write-Host "Environment variables that must be set manually:" -ForegroundColor Blue -} else { +} +else { Write-Host "Environment variables set:" -ForegroundColor Blue Write-Host ($Variables | Out-String) if ($PrependPath) { @@ -43,28 +46,8 @@ if ($env:GITHUB_ACTIONS) { Write-Host "GitHub Actions detected. Logging commands will be used to propagate environment variables and prepend path." } -function Add-GitHubActionsEnvVariable { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [AllowEmptyString()] - [string]$Value - ) - - if ([string]::IsNullOrWhiteSpace($Name)) { - throw "GitHub Actions environment variable names must not be empty." - } - - $utf8NoBom = [System.Text.UTF8Encoding]::new($false) - $delimiter = [guid]::NewGuid().ToString('N') - [System.IO.File]::AppendAllText($Path, "$Name<<$delimiter`n$Value`n$delimiter`n", $utf8NoBom) -} - $CmdEnvScript = '' -$Variables.GetEnumerator() |% { +$Variables.GetEnumerator() | % { Set-Item -LiteralPath env:$($_.Key) -Value $_.Value # If we're running in a cloud CI, set these environment variables so they propagate. @@ -72,7 +55,7 @@ $Variables.GetEnumerator() |% { Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)" } if ($env:GITHUB_ACTIONS) { - Add-GitHubActionsEnvVariable -Path $env:GITHUB_ENV -Name $_.Key -Value ([string]$_.Value) + Add-GitHubActionsEnvVariable -Name $_.Key -Value ([string]$_.Value) } if ($cmdInstructions) { @@ -88,7 +71,7 @@ if ($IsMacOS -or $IsLinux) { } if ($PrependPath) { - $PrependPath |% { + $PrependPath | % { $newPathValue = "$_$pathDelimiter$env:PATH" Set-Item -LiteralPath env:PATH -Value $newPathValue if ($cmdInstructions) { @@ -99,7 +82,7 @@ if ($PrependPath) { Write-Host "##vso[task.prependpath]$_" } if ($env:GITHUB_ACTIONS) { - Add-GitHubActionsEnvVariable -Path $env:GITHUB_ENV -Name PATH -Value $newPathValue + Add-GitHubActionsEnvVariable -Name PATH -Value $newPathValue } $CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%" diff --git a/tools/variables/_define.ps1 b/tools/variables/_define.ps1 index 4a8503d..dfa9cba 100644 --- a/tools/variables/_define.ps1 +++ b/tools/variables/_define.ps1 @@ -11,25 +11,7 @@ param ( ) -function Add-GitHubActionsEnvVariable { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [AllowEmptyString()] - [string]$Value - ) - - if ([string]::IsNullOrWhiteSpace($Name)) { - throw "GitHub Actions environment variable names must not be empty." - } - - $utf8NoBom = [System.Text.UTF8Encoding]::new($false) - $delimiter = [guid]::NewGuid().ToString('N') - [System.IO.File]::AppendAllText($Path, "$Name<<$delimiter`n$Value`n$delimiter`n", $utf8NoBom) -} +. "$PSScriptRoot\..\GitHubActions.ps1" (& "$PSScriptRoot\_all.ps1").GetEnumerator() |% { # Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive. @@ -44,7 +26,7 @@ function Add-GitHubActionsEnvVariable { # and the second that works across jobs and stages but must be fully qualified when referenced. Write-Host "##vso[task.setvariable variable=$keyCaps;isOutput=true]$($_.Value)" } elseif ($env:GITHUB_ACTIONS) { - Add-GitHubActionsEnvVariable -Path $env:GITHUB_ENV -Name $keyCaps -Value ([string]$_.Value) + Add-GitHubActionsEnvVariable -Name $keyCaps -Value ([string]$_.Value) } Set-Item -LiteralPath "env:$keyCaps" -Value $_.Value } From 4a5b860e5ed5e9a41599955a8d16f7dba1475028 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:02:26 -0600 Subject: [PATCH 17/48] Update dependency powershell to v7.6.3 (#530) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index f88bae6..f82fc36 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "powershell": { - "version": "7.6.2", + "version": "7.6.3", "commands": [ "pwsh" ], From 3fcf71e38c502f58e3329a44c247d7d11ef74ae4 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 18 Jun 2026 12:30:32 -0600 Subject: [PATCH 18/48] Update actions/checkout action to v7 --- .github/workflows/build.yml | 104 ++++++++-------- .github/workflows/copilot-setup-steps.yml | 30 ++--- .github/workflows/docs.yml | 30 ++--- .github/workflows/libtemplate-update.yml | 140 +++++++++++----------- 4 files changed, 152 insertions(+), 152 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 69e7ae4..110b0af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,9 +3,9 @@ name: ๐Ÿญ Build on: push: branches: - - main - - 'v*.*' - - validate/* + - main + - "v*.*" + - validate/* pull_request: workflow_dispatch: @@ -23,60 +23,60 @@ jobs: fail-fast: false matrix: os: - - ubuntu-24.04 - - macOS-15 - - windows-2025 + - ubuntu-24.04 + - macOS-15 + - windows-2025 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: โš™ Install prerequisites - run: | - ./init.ps1 -UpgradePrerequisites - dotnet --info + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - name: โš™ Install prerequisites + run: | + ./init.ps1 -UpgradePrerequisites + dotnet --info - # Print mono version if it is present. - if (Get-Command mono -ErrorAction SilentlyContinue) { - mono --version - } - shell: pwsh - - name: โš™๏ธ Set pipeline variables based on source - run: tools/variables/_define.ps1 - shell: pwsh - - name: ๐Ÿ›  build - run: dotnet build -t:build,pack --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog" - - name: ๐Ÿงช test - run: tools/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }} - shell: pwsh - - name: ๐Ÿ’…๐Ÿป Verify formatted code - run: dotnet format --verify-no-changes --no-restore - shell: pwsh - if: runner.os == 'Linux' - - name: ๐Ÿ“š Verify docfx build - run: dotnet docfx docfx/docfx.json --warningsAsErrors --disableGitFeatures - if: runner.os == 'Linux' - - name: โš™ Update pipeline variables based on build outputs - run: tools/variables/_define.ps1 - shell: pwsh - - name: ๐Ÿ“ข Publish artifacts - uses: ./.github/actions/publish-artifacts - if: cancelled() == false - - name: ๐Ÿ“ข Publish code coverage results to codecov.io - run: | - if ('${{ secrets.CODECOV_TOKEN }}') { - ./tools/publish-CodeCov.ps1 -CodeCovToken '${{ secrets.CODECOV_TOKEN }}' -PathToCodeCoverage "${{ runner.temp }}/_artifacts/coverageResults" -Name "${{ runner.os }} Coverage Results" -Flags "${{ runner.os }}" - } - shell: pwsh - timeout-minutes: 3 - continue-on-error: true + # Print mono version if it is present. + if (Get-Command mono -ErrorAction SilentlyContinue) { + mono --version + } + shell: pwsh + - name: โš™๏ธ Set pipeline variables based on source + run: tools/variables/_define.ps1 + shell: pwsh + - name: ๐Ÿ›  build + run: dotnet build -t:build,pack --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog" + - name: ๐Ÿงช test + run: tools/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }} + shell: pwsh + - name: ๐Ÿ’…๐Ÿป Verify formatted code + run: dotnet format --verify-no-changes --no-restore + shell: pwsh + if: runner.os == 'Linux' + - name: ๐Ÿ“š Verify docfx build + run: dotnet docfx docfx/docfx.json --warningsAsErrors --disableGitFeatures + if: runner.os == 'Linux' + - name: โš™ Update pipeline variables based on build outputs + run: tools/variables/_define.ps1 + shell: pwsh + - name: ๐Ÿ“ข Publish artifacts + uses: ./.github/actions/publish-artifacts + if: cancelled() == false + - name: ๐Ÿ“ข Publish code coverage results to codecov.io + run: | + if ('${{ secrets.CODECOV_TOKEN }}') { + ./tools/publish-CodeCov.ps1 -CodeCovToken '${{ secrets.CODECOV_TOKEN }}' -PathToCodeCoverage "${{ runner.temp }}/_artifacts/coverageResults" -Name "${{ runner.os }} Coverage Results" -Flags "${{ runner.os }}" + } + shell: pwsh + timeout-minutes: 3 + continue-on-error: true docs: name: ๐Ÿ“ƒ Docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: ๐Ÿ”— Markup Link Checker (mlc) - uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 - with: - args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/ + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: ๐Ÿ”— Markup Link Checker (mlc) + uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 + with: + args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/ diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index d933871..e61bfd5 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -6,12 +6,12 @@ on: workflow_dispatch: push: branches: - - main + - main paths: - - .github/workflows/copilot-setup-steps.yml + - .github/workflows/copilot-setup-steps.yml pull_request: paths: - - .github/workflows/copilot-setup-steps.yml + - .github/workflows/copilot-setup-steps.yml jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. @@ -26,16 +26,16 @@ jobs: # You can define any steps you want, and they will run before the agent starts. # If you do not check out your code, Copilot will do this for you. steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: โš™ Install prerequisites - run: | - ./init.ps1 -UpgradePrerequisites -NoNuGetCredProvider - dotnet --info + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - name: โš™ Install prerequisites + run: | + ./init.ps1 -UpgradePrerequisites -NoNuGetCredProvider + dotnet --info - # Print mono version if it is present. - if (Get-Command mono -ErrorAction SilentlyContinue) { - mono --version - } - shell: pwsh + # Print mono version if it is present. + if (Get-Command mono -ErrorAction SilentlyContinue) { + mono --version + } + shell: pwsh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 99b3c05..6f0fbaa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ name: ๐Ÿ“š Docs on: push: branches: - - main + - main # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. @@ -24,20 +24,20 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: โš™ Install prerequisites - run: ./init.ps1 -UpgradePrerequisites + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - name: โš™ Install prerequisites + run: ./init.ps1 -UpgradePrerequisites - - run: dotnet docfx docfx/docfx.json - name: ๐Ÿ“š Generate documentation + - run: dotnet docfx docfx/docfx.json + name: ๐Ÿ“š Generate documentation - - name: Upload artifact - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 - with: - path: docfx/_site + - name: Upload artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: docfx/_site - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/libtemplate-update.yml b/.github/workflows/libtemplate-update.yml index fc8a631..6336d63 100644 --- a/.github/workflows/libtemplate-update.yml +++ b/.github/workflows/libtemplate-update.yml @@ -7,7 +7,7 @@ name: โ›œ Library.Template update on: schedule: - - cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST) + - cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST) workflow_dispatch: jobs: @@ -17,82 +17,82 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: merge - id: merge - shell: pwsh - run: | - $LibTemplateBranch = & ./tools/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $LibTemplateCommit = git rev-parse FETCH_HEAD - git diff --stat ...FETCH_HEAD - - if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) { - Write-Host "There are no Library.Template updates to merge." - echo "uptodate=true" >> $env:GITHUB_OUTPUT - exit 0 - } + - name: merge + id: merge + shell: pwsh + run: | + $LibTemplateBranch = & ./tools/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } - # Pushing commits that add or change files under .github/workflows will cause our workflow to fail. - # But it usually isn't necessary because the target branch already has (or doesn't have) these changes. - # So if the merge doesn't bring in any changes to these files, try the merge locally and push that - # to keep github happy. - if ((git rev-list FETCH_HEAD ^HEAD --count -- .github/workflows) -eq 0) { - # Indeed there are no changes in that area. So merge locally to try to appease GitHub. - git checkout -b auto/libtemplateUpdate - git config user.name "Andrew Arnott" - git config user.email "andrewarnott@live.com" - git merge FETCH_HEAD + git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch if ($LASTEXITCODE -ne 0) { - Write-Host "Merge conflicts prevent creating the pull request. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." - exit 2 + exit $LASTEXITCODE } + $LibTemplateCommit = git rev-parse FETCH_HEAD + git diff --stat ...FETCH_HEAD - git -c http.extraheader="AUTHORIZATION: bearer $env:GH_TOKEN" push origin -u HEAD - } else { - Write-Host "Changes to github workflows are included in this update. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." - exit 1 - } - - name: pull request - shell: pwsh - if: success() && steps.merge.outputs.uptodate != 'true' - run: | - # If there is already an active pull request, don't create a new one. - $existingPR = gh pr list -H auto/libtemplateUpdate --json url | ConvertFrom-Json - if ($existingPR) { - Write-Host "::warning::Skipping pull request creation because one already exists at $($existingPR[0].url)" - exit 0 - } + if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) { + Write-Host "There are no Library.Template updates to merge." + echo "uptodate=true" >> $env:GITHUB_OUTPUT + exit 0 + } + + # Pushing commits that add or change files under .github/workflows will cause our workflow to fail. + # But it usually isn't necessary because the target branch already has (or doesn't have) these changes. + # So if the merge doesn't bring in any changes to these files, try the merge locally and push that + # to keep github happy. + if ((git rev-list FETCH_HEAD ^HEAD --count -- .github/workflows) -eq 0) { + # Indeed there are no changes in that area. So merge locally to try to appease GitHub. + git checkout -b auto/libtemplateUpdate + git config user.name "Andrew Arnott" + git config user.email "andrewarnott@live.com" + git merge FETCH_HEAD + if ($LASTEXITCODE -ne 0) { + Write-Host "Merge conflicts prevent creating the pull request. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." + exit 2 + } + + git -c http.extraheader="AUTHORIZATION: bearer $env:GH_TOKEN" push origin -u HEAD + } else { + Write-Host "Changes to github workflows are included in this update. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." + exit 1 + } + - name: pull request + shell: pwsh + if: success() && steps.merge.outputs.uptodate != 'true' + run: | + # If there is already an active pull request, don't create a new one. + $existingPR = gh pr list -H auto/libtemplateUpdate --json url | ConvertFrom-Json + if ($existingPR) { + Write-Host "::warning::Skipping pull request creation because one already exists at $($existingPR[0].url)" + exit 0 + } - $prTitle = "Merge latest Library.Template" - $prBody = "This merges the latest features and fixes from [Library.Template's branch](https://github.com/AArnott/Library.Template/tree/). + $prTitle = "Merge latest Library.Template" + $prBody = "This merges the latest features and fixes from [Library.Template's branch](https://github.com/AArnott/Library.Template/tree/). - โš ๏ธ Do **not** squash this pull request when completing it. You must *merge* it. + โš ๏ธ Do **not** squash this pull request when completing it. You must *merge* it. -
- Merge conflicts? - Resolve merge conflicts locally by carrying out these steps: +
+ Merge conflicts? + Resolve merge conflicts locally by carrying out these steps: - ``` - git fetch - git checkout auto/libtemplateUpdate - git merge origin/main - # resolve conflicts - git commit - git push - ``` -
" + ``` + git fetch + git checkout auto/libtemplateUpdate + git merge origin/main + # resolve conflicts + git commit + git push + ``` +
" - gh pr create -H auto/libtemplateUpdate -b $prBody -t $prTitle - env: - GH_TOKEN: ${{ github.token }} + gh pr create -H auto/libtemplateUpdate -b $prBody -t $prTitle + env: + GH_TOKEN: ${{ github.token }} From 93197387762654785bc0772c82bbd28f418a6fb9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:13:38 -0600 Subject: [PATCH 19/48] Update nbgv and nerdbank.gitversioning updates to 3.10.85 (531) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- Directory.Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index f82fc36..2ddce27 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "nbgv": { - "version": "3.9.50", + "version": "3.10.85", "commands": [ "nbgv" ], diff --git a/Directory.Packages.props b/Directory.Packages.props index 8630c45..82ce8d7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - +
From 3f53cb6c23aacacd3ba715a1f01460eee869b527 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 20 Jun 2026 11:27:25 -0600 Subject: [PATCH 20/48] Revert unintended whitespace changes --- .github/workflows/build.yml | 104 ++++++++-------- .github/workflows/copilot-setup-steps.yml | 30 ++--- .github/workflows/docs.yml | 30 ++--- .github/workflows/libtemplate-update.yml | 140 +++++++++++----------- 4 files changed, 152 insertions(+), 152 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 110b0af..94a48d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,9 +3,9 @@ name: ๐Ÿญ Build on: push: branches: - - main - - "v*.*" - - validate/* + - main + - 'v*.*' + - validate/* pull_request: workflow_dispatch: @@ -23,60 +23,60 @@ jobs: fail-fast: false matrix: os: - - ubuntu-24.04 - - macOS-15 - - windows-2025 + - ubuntu-24.04 + - macOS-15 + - windows-2025 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: โš™ Install prerequisites - run: | - ./init.ps1 -UpgradePrerequisites - dotnet --info + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - name: โš™ Install prerequisites + run: | + ./init.ps1 -UpgradePrerequisites + dotnet --info - # Print mono version if it is present. - if (Get-Command mono -ErrorAction SilentlyContinue) { - mono --version - } - shell: pwsh - - name: โš™๏ธ Set pipeline variables based on source - run: tools/variables/_define.ps1 - shell: pwsh - - name: ๐Ÿ›  build - run: dotnet build -t:build,pack --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog" - - name: ๐Ÿงช test - run: tools/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }} - shell: pwsh - - name: ๐Ÿ’…๐Ÿป Verify formatted code - run: dotnet format --verify-no-changes --no-restore - shell: pwsh - if: runner.os == 'Linux' - - name: ๐Ÿ“š Verify docfx build - run: dotnet docfx docfx/docfx.json --warningsAsErrors --disableGitFeatures - if: runner.os == 'Linux' - - name: โš™ Update pipeline variables based on build outputs - run: tools/variables/_define.ps1 - shell: pwsh - - name: ๐Ÿ“ข Publish artifacts - uses: ./.github/actions/publish-artifacts - if: cancelled() == false - - name: ๐Ÿ“ข Publish code coverage results to codecov.io - run: | - if ('${{ secrets.CODECOV_TOKEN }}') { - ./tools/publish-CodeCov.ps1 -CodeCovToken '${{ secrets.CODECOV_TOKEN }}' -PathToCodeCoverage "${{ runner.temp }}/_artifacts/coverageResults" -Name "${{ runner.os }} Coverage Results" -Flags "${{ runner.os }}" - } - shell: pwsh - timeout-minutes: 3 - continue-on-error: true + # Print mono version if it is present. + if (Get-Command mono -ErrorAction SilentlyContinue) { + mono --version + } + shell: pwsh + - name: โš™๏ธ Set pipeline variables based on source + run: tools/variables/_define.ps1 + shell: pwsh + - name: ๐Ÿ›  build + run: dotnet build -t:build,pack --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog" + - name: ๐Ÿงช test + run: tools/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }} + shell: pwsh + - name: ๐Ÿ’…๐Ÿป Verify formatted code + run: dotnet format --verify-no-changes --no-restore + shell: pwsh + if: runner.os == 'Linux' + - name: ๐Ÿ“š Verify docfx build + run: dotnet docfx docfx/docfx.json --warningsAsErrors --disableGitFeatures + if: runner.os == 'Linux' + - name: โš™ Update pipeline variables based on build outputs + run: tools/variables/_define.ps1 + shell: pwsh + - name: ๐Ÿ“ข Publish artifacts + uses: ./.github/actions/publish-artifacts + if: cancelled() == false + - name: ๐Ÿ“ข Publish code coverage results to codecov.io + run: | + if ('${{ secrets.CODECOV_TOKEN }}') { + ./tools/publish-CodeCov.ps1 -CodeCovToken '${{ secrets.CODECOV_TOKEN }}' -PathToCodeCoverage "${{ runner.temp }}/_artifacts/coverageResults" -Name "${{ runner.os }} Coverage Results" -Flags "${{ runner.os }}" + } + shell: pwsh + timeout-minutes: 3 + continue-on-error: true docs: name: ๐Ÿ“ƒ Docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: ๐Ÿ”— Markup Link Checker (mlc) - uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 - with: - args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/ + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: ๐Ÿ”— Markup Link Checker (mlc) + uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 + with: + args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/ diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index e61bfd5..c0436e8 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -6,12 +6,12 @@ on: workflow_dispatch: push: branches: - - main + - main paths: - - .github/workflows/copilot-setup-steps.yml + - .github/workflows/copilot-setup-steps.yml pull_request: paths: - - .github/workflows/copilot-setup-steps.yml + - .github/workflows/copilot-setup-steps.yml jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. @@ -26,16 +26,16 @@ jobs: # You can define any steps you want, and they will run before the agent starts. # If you do not check out your code, Copilot will do this for you. steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: โš™ Install prerequisites - run: | - ./init.ps1 -UpgradePrerequisites -NoNuGetCredProvider - dotnet --info + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - name: โš™ Install prerequisites + run: | + ./init.ps1 -UpgradePrerequisites -NoNuGetCredProvider + dotnet --info - # Print mono version if it is present. - if (Get-Command mono -ErrorAction SilentlyContinue) { - mono --version - } - shell: pwsh + # Print mono version if it is present. + if (Get-Command mono -ErrorAction SilentlyContinue) { + mono --version + } + shell: pwsh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6f0fbaa..9729dc3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ name: ๐Ÿ“š Docs on: push: branches: - - main + - main # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. @@ -24,20 +24,20 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: โš™ Install prerequisites - run: ./init.ps1 -UpgradePrerequisites + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - name: โš™ Install prerequisites + run: ./init.ps1 -UpgradePrerequisites - - run: dotnet docfx docfx/docfx.json - name: ๐Ÿ“š Generate documentation + - run: dotnet docfx docfx/docfx.json + name: ๐Ÿ“š Generate documentation - - name: Upload artifact - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 - with: - path: docfx/_site + - name: Upload artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: docfx/_site - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/libtemplate-update.yml b/.github/workflows/libtemplate-update.yml index 6336d63..dfd3f57 100644 --- a/.github/workflows/libtemplate-update.yml +++ b/.github/workflows/libtemplate-update.yml @@ -7,7 +7,7 @@ name: โ›œ Library.Template update on: schedule: - - cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST) + - cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST) workflow_dispatch: jobs: @@ -17,82 +17,82 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: merge - id: merge - shell: pwsh - run: | - $LibTemplateBranch = & ./tools/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } + - name: merge + id: merge + shell: pwsh + run: | + $LibTemplateBranch = & ./tools/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } - git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $LibTemplateCommit = git rev-parse FETCH_HEAD - git diff --stat ...FETCH_HEAD + git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $LibTemplateCommit = git rev-parse FETCH_HEAD + git diff --stat ...FETCH_HEAD - if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) { - Write-Host "There are no Library.Template updates to merge." - echo "uptodate=true" >> $env:GITHUB_OUTPUT - exit 0 - } + if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) { + Write-Host "There are no Library.Template updates to merge." + echo "uptodate=true" >> $env:GITHUB_OUTPUT + exit 0 + } - # Pushing commits that add or change files under .github/workflows will cause our workflow to fail. - # But it usually isn't necessary because the target branch already has (or doesn't have) these changes. - # So if the merge doesn't bring in any changes to these files, try the merge locally and push that - # to keep github happy. - if ((git rev-list FETCH_HEAD ^HEAD --count -- .github/workflows) -eq 0) { - # Indeed there are no changes in that area. So merge locally to try to appease GitHub. - git checkout -b auto/libtemplateUpdate - git config user.name "Andrew Arnott" - git config user.email "andrewarnott@live.com" - git merge FETCH_HEAD - if ($LASTEXITCODE -ne 0) { - Write-Host "Merge conflicts prevent creating the pull request. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." - exit 2 - } - - git -c http.extraheader="AUTHORIZATION: bearer $env:GH_TOKEN" push origin -u HEAD - } else { - Write-Host "Changes to github workflows are included in this update. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." - exit 1 - } - - name: pull request - shell: pwsh - if: success() && steps.merge.outputs.uptodate != 'true' - run: | - # If there is already an active pull request, don't create a new one. - $existingPR = gh pr list -H auto/libtemplateUpdate --json url | ConvertFrom-Json - if ($existingPR) { - Write-Host "::warning::Skipping pull request creation because one already exists at $($existingPR[0].url)" - exit 0 + # Pushing commits that add or change files under .github/workflows will cause our workflow to fail. + # But it usually isn't necessary because the target branch already has (or doesn't have) these changes. + # So if the merge doesn't bring in any changes to these files, try the merge locally and push that + # to keep github happy. + if ((git rev-list FETCH_HEAD ^HEAD --count -- .github/workflows) -eq 0) { + # Indeed there are no changes in that area. So merge locally to try to appease GitHub. + git checkout -b auto/libtemplateUpdate + git config user.name "Andrew Arnott" + git config user.email "andrewarnott@live.com" + git merge FETCH_HEAD + if ($LASTEXITCODE -ne 0) { + Write-Host "Merge conflicts prevent creating the pull request. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." + exit 2 } - $prTitle = "Merge latest Library.Template" - $prBody = "This merges the latest features and fixes from [Library.Template's branch](https://github.com/AArnott/Library.Template/tree/). + git -c http.extraheader="AUTHORIZATION: bearer $env:GH_TOKEN" push origin -u HEAD + } else { + Write-Host "Changes to github workflows are included in this update. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." + exit 1 + } + - name: pull request + shell: pwsh + if: success() && steps.merge.outputs.uptodate != 'true' + run: | + # If there is already an active pull request, don't create a new one. + $existingPR = gh pr list -H auto/libtemplateUpdate --json url | ConvertFrom-Json + if ($existingPR) { + Write-Host "::warning::Skipping pull request creation because one already exists at $($existingPR[0].url)" + exit 0 + } + + $prTitle = "Merge latest Library.Template" + $prBody = "This merges the latest features and fixes from [Library.Template's branch](https://github.com/AArnott/Library.Template/tree/). - โš ๏ธ Do **not** squash this pull request when completing it. You must *merge* it. + โš ๏ธ Do **not** squash this pull request when completing it. You must *merge* it. -
- Merge conflicts? - Resolve merge conflicts locally by carrying out these steps: +
+ Merge conflicts? + Resolve merge conflicts locally by carrying out these steps: - ``` - git fetch - git checkout auto/libtemplateUpdate - git merge origin/main - # resolve conflicts - git commit - git push - ``` -
" + ``` + git fetch + git checkout auto/libtemplateUpdate + git merge origin/main + # resolve conflicts + git commit + git push + ``` +
" - gh pr create -H auto/libtemplateUpdate -b $prBody -t $prTitle - env: - GH_TOKEN: ${{ github.token }} + gh pr create -H auto/libtemplateUpdate -b $prBody -t $prTitle + env: + GH_TOKEN: ${{ github.token }} From 8a8921ae310197c01b11573034e4ac859269ed70 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 22 Jun 2026 11:11:10 -0600 Subject: [PATCH 21/48] Update jobs to ubuntu 24 --- azure-pipelines/archive-sourcecode.yml | 2 +- azure-pipelines/libtemplate-update.yml | 2 +- azure-pipelines/official.yml | 2 +- azure-pipelines/prepare-insertion-stages.yml | 2 +- azure-pipelines/release.yml | 4 ++-- azure-pipelines/unofficial.yml | 2 +- azure-pipelines/vs-validation.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/azure-pipelines/archive-sourcecode.yml b/azure-pipelines/archive-sourcecode.yml index 211881f..9e075df 100644 --- a/azure-pipelines/archive-sourcecode.yml +++ b/azure-pipelines/archive-sourcecode.yml @@ -45,7 +45,7 @@ extends: pool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 + - ImageOverride -equals 1ESPT-Ubuntu24.04 os: Linux steps: diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml index 384be7c..4efbd20 100644 --- a/azure-pipelines/libtemplate-update.yml +++ b/azure-pipelines/libtemplate-update.yml @@ -45,7 +45,7 @@ extends: pool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 + - ImageOverride -equals 1ESPT-Ubuntu24.04 os: Linux steps: - checkout: self diff --git a/azure-pipelines/official.yml b/azure-pipelines/official.yml index 22a5d6a..38e95f2 100644 --- a/azure-pipelines/official.yml +++ b/azure-pipelines/official.yml @@ -74,7 +74,7 @@ extends: linuxPool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 + - ImageOverride -equals 1ESPT-Ubuntu24.04 os: Linux macOSPool: name: Azure Pipelines diff --git a/azure-pipelines/prepare-insertion-stages.yml b/azure-pipelines/prepare-insertion-stages.yml index 9f934f9..675912b 100644 --- a/azure-pipelines/prepare-insertion-stages.yml +++ b/azure-pipelines/prepare-insertion-stages.yml @@ -51,7 +51,7 @@ stages: pool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 + - ImageOverride -equals 1ESPT-Ubuntu24.04 os: Linux templateContext: outputs: diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index edc5652..a16f98a 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -31,7 +31,7 @@ extends: pool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 + - ImageOverride -equals 1ESPT-Ubuntu24.04 os: Linux templateContext: outputs: @@ -54,7 +54,7 @@ extends: pool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 + - ImageOverride -equals 1ESPT-Ubuntu24.04 os: Linux templateContext: type: releaseJob diff --git a/azure-pipelines/unofficial.yml b/azure-pipelines/unofficial.yml index 36aaf6e..5175253 100644 --- a/azure-pipelines/unofficial.yml +++ b/azure-pipelines/unofficial.yml @@ -87,7 +87,7 @@ extends: linuxPool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 + - ImageOverride -equals 1ESPT-Ubuntu24.04 os: Linux macOSPool: name: Azure Pipelines diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml index 6a839d4..0eb2c2a 100644 --- a/azure-pipelines/vs-validation.yml +++ b/azure-pipelines/vs-validation.yml @@ -48,7 +48,7 @@ extends: linuxPool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 + - ImageOverride -equals 1ESPT-Ubuntu24.04 os: Linux macOSPool: name: Azure Pipelines From d7904f8c308a457ae2d06adea4040c1f8f2dc275 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 07:25:38 -0600 Subject: [PATCH 22/48] Update Dockerfile and global.json updates (532) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 08c0185..635ed8c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions -FROM mcr.microsoft.com/dotnet/sdk:10.0.301@sha256:548d93f8a18a1acbe6cc127bc4f47281430d34a9e35c18afa80a8d6741c2adc3 +FROM mcr.microsoft.com/dotnet/sdk:10.0.301@sha256:ea8bde36c11b6e7eec2656d0e59101d4462f6bd630730f2c8201ed0572b295d5 # Installing mono makes `dotnet test` work without errors even for net472. # But installing it takes a long time, so it's excluded by default. From a1ba5694dbf42e55db3a4da4a10896a8c8ca586e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:33:41 +0000 Subject: [PATCH 23/48] Update Microsoft Testing Platform to 2.3.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 82ce8d7..fa4d203 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true true - 2.2.3 + 2.3.0 From 35daa41ee3d87b5ac610ef03b49733ca77613695 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 7 Jul 2026 13:40:09 -0600 Subject: [PATCH 24/48] Pin Microsoft.Testing.Extensions.Telemetry to the common MTP version --- Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index fa4d203..31eb811 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,6 +13,7 @@ + From eba6257dfab62a90bbc00fbb9778d7a3382a0f9a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 7 Jul 2026 15:48:59 -0600 Subject: [PATCH 25/48] Revert "Use default configuration for Network Isolation" (#534) --- azure-pipelines/archive-sourcecode.yml | 2 ++ azure-pipelines/libtemplate-update.yml | 2 ++ azure-pipelines/official.yml | 2 ++ azure-pipelines/release.yml | 2 ++ azure-pipelines/unofficial.yml | 2 ++ azure-pipelines/vs-insertion.yml | 2 ++ azure-pipelines/vs-validation.yml | 2 ++ 7 files changed, 14 insertions(+) diff --git a/azure-pipelines/archive-sourcecode.yml b/azure-pipelines/archive-sourcecode.yml index 9e075df..cb8d68e 100644 --- a/azure-pipelines/archive-sourcecode.yml +++ b/azure-pipelines/archive-sourcecode.yml @@ -35,6 +35,8 @@ variables: extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml index 4efbd20..acbfbac 100644 --- a/azure-pipelines/libtemplate-update.yml +++ b/azure-pipelines/libtemplate-update.yml @@ -30,6 +30,8 @@ variables: extends: template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 sdl: sourceAnalysisPool: name: AzurePipelines-EO diff --git a/azure-pipelines/official.yml b/azure-pipelines/official.yml index 38e95f2..99815eb 100644 --- a/azure-pipelines/official.yml +++ b/azure-pipelines/official.yml @@ -45,6 +45,8 @@ variables: extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable codeSignValidation: diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index a16f98a..8b7f8c9 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -20,6 +20,8 @@ variables: extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable diff --git a/azure-pipelines/unofficial.yml b/azure-pipelines/unofficial.yml index 5175253..efc60f3 100644 --- a/azure-pipelines/unofficial.yml +++ b/azure-pipelines/unofficial.yml @@ -55,6 +55,8 @@ variables: extends: template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable credscan: diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index 5e5a476..edb114c 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -22,6 +22,8 @@ variables: extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable sbom: diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml index 0eb2c2a..3f2a194 100644 --- a/azure-pipelines/vs-validation.yml +++ b/azure-pipelines/vs-validation.yml @@ -26,6 +26,8 @@ variables: extends: template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable credscan: From 4299650d88d492c5d1725c81c55e4e734e477c73 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 7 Jul 2026 16:25:20 -0600 Subject: [PATCH 26/48] Fix typo --- .github/skills/bundle-dependency-prs/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/skills/bundle-dependency-prs/SKILL.md b/.github/skills/bundle-dependency-prs/SKILL.md index 48dedbe..3fec668 100644 --- a/.github/skills/bundle-dependency-prs/SKILL.md +++ b/.github/skills/bundle-dependency-prs/SKILL.md @@ -27,7 +27,7 @@ Before aggregating PRs, first try to fix any individual dependency update PRs wi 1. For the dependency PRs with failing build or test PR checks, check out their source branch and fix any issues. 2. Push your fixes as fresh commits to the individual dependency PRs. - If pushing to a repo in the `microsoft` org when the PR is authord by `renovate`, follow-up the push with a PR comment that says exactly this: "/azp run" which triggers PR checks to re-run. + If pushing to a repo in the `microsoft` org when the PR is authored by `renovate`, follow-up the push with a PR comment that says exactly this: "/azp run" which triggers PR checks to re-run. 3. If you can't fix a particular PR, add a comment to the PR describing your attempt and outcome. ## Group dependency PRs that are ready to go From 90ac1ee05f361d25ff36e42ce217316768c2ee11 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 7 Jul 2026 16:32:43 -0600 Subject: [PATCH 27/48] Use preferred pattern for appending to PATH in GitHub Actions --- tools/GitHubActions.ps1 | 15 +++++++++++++++ tools/Set-EnvVars.ps1 | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/tools/GitHubActions.ps1 b/tools/GitHubActions.ps1 index c742a58..8678d0d 100644 --- a/tools/GitHubActions.ps1 +++ b/tools/GitHubActions.ps1 @@ -16,3 +16,18 @@ function Add-GitHubActionsEnvVariable { $delimiter = [guid]::NewGuid().ToString('N') [System.IO.File]::AppendAllText($Path, "$Name<<$delimiter`n$Value`n$delimiter`n", $utf8NoBom) } + +function Add-GitHubActionsPath { + param( + [string]$Path = $env:GITHUB_PATH, + [Parameter(Mandatory = $true)] + [string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Path)) { + throw "GitHub Actions path file must not be empty." + } + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($Path, "$Value`n", $utf8NoBom) +} diff --git a/tools/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1 index 0816c0e..c2d95b8 100644 --- a/tools/Set-EnvVars.ps1 +++ b/tools/Set-EnvVars.ps1 @@ -82,7 +82,7 @@ if ($PrependPath) { Write-Host "##vso[task.prependpath]$_" } if ($env:GITHUB_ACTIONS) { - Add-GitHubActionsEnvVariable -Name PATH -Value $newPathValue + Add-GitHubActionsPath -Value $_ } $CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%" From 5a0a13fe6340618056b81b8e1d90092fc012ff03 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 7 Jul 2026 16:51:06 -0600 Subject: [PATCH 28/48] Avoid loading GitHubActions.ps1 on Azure Pipelines --- tools/variables/_define.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/variables/_define.ps1 b/tools/variables/_define.ps1 index dfa9cba..90fd080 100644 --- a/tools/variables/_define.ps1 +++ b/tools/variables/_define.ps1 @@ -11,7 +11,9 @@ param ( ) -. "$PSScriptRoot\..\GitHubActions.ps1" +if ($env:GITHUB_ACTIONS) { + . "$PSScriptRoot\..\GitHubActions.ps1" +} (& "$PSScriptRoot\_all.ps1").GetEnumerator() |% { # Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive. From 097060adc6c79e0aa7cb16ec4995e868c7350b44 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 7 Jul 2026 17:42:55 -0600 Subject: [PATCH 29/48] Validate Path parameter in Add-GitHubActionsEnvVariable --- tools/GitHubActions.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/GitHubActions.ps1 b/tools/GitHubActions.ps1 index 8678d0d..1546d99 100644 --- a/tools/GitHubActions.ps1 +++ b/tools/GitHubActions.ps1 @@ -8,6 +8,10 @@ function Add-GitHubActionsEnvVariable { [string]$Value ) + if ([string]::IsNullOrWhiteSpace($Path)) { + throw "GitHub Actions environment file must not be empty." + } + if ([string]::IsNullOrWhiteSpace($Name)) { throw "GitHub Actions environment variable names must not be empty." } From 5cd226ad1977e1fc3150ccf6614b3afd81c0b3b4 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 07:46:02 -0600 Subject: [PATCH 30/48] Fix 1ES PT nuget push --- azure-pipelines/prepare-insertion-stages.yml | 2 +- azure-pipelines/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines/prepare-insertion-stages.yml b/azure-pipelines/prepare-insertion-stages.yml index 675912b..d772332 100644 --- a/azure-pipelines/prepare-insertion-stages.yml +++ b/azure-pipelines/prepare-insertion-stages.yml @@ -51,7 +51,7 @@ stages: pool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu24.04 + - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary os: Linux templateContext: outputs: diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index 8b7f8c9..dda671c 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -33,7 +33,7 @@ extends: pool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu24.04 + - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary os: Linux templateContext: outputs: From b5153f6c64e58cc4cc09e44c3e8a0a76235cfdae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:49:19 -0600 Subject: [PATCH 31/48] Update dependency dotnet-coverage to v18.9.0 (536) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 2ddce27..9484456 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "dotnet-coverage": { - "version": "18.8.0", + "version": "18.9.0", "commands": [ "dotnet-coverage" ], From 42c5cea6cd1a74c3fc6c41c2f92018ec6faba81c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:55:40 +0000 Subject: [PATCH 32/48] Update dependency Microsoft.Testing.Extensions.CodeCoverage to 18.9.0 (537) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 31eb811..2e095f2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + From e6e3c19ad90332fe95b2c9e84aafef423a8b6ac3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 08:23:09 -0600 Subject: [PATCH 33/48] Revert "Update dependency dotnet-coverage to v18.9.0 (536)" This reverts commit b5153f6c64e58cc4cc09e44c3e8a0a76235cfdae. The dependency isn't allowed per CFS policy yet. --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9484456..2ddce27 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "dotnet-coverage": { - "version": "18.9.0", + "version": "18.8.0", "commands": [ "dotnet-coverage" ], From c43fba65f1989efc05d45b822c32f657705d766c Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 08:42:01 -0600 Subject: [PATCH 34/48] Prevent empty paths from being added to the PATH --- tools/GitHubActions.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/GitHubActions.ps1 b/tools/GitHubActions.ps1 index 1546d99..fbe5439 100644 --- a/tools/GitHubActions.ps1 +++ b/tools/GitHubActions.ps1 @@ -32,6 +32,10 @@ function Add-GitHubActionsPath { throw "GitHub Actions path file must not be empty." } + if ([string]::IsNullOrWhiteSpace($Value)) { + throw "GitHub Actions path entries must not be empty." + } + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::AppendAllText($Path, "$Value`n", $utf8NoBom) } From a4d4554915ecf1e181db41e2274969c21761d1f4 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 09:15:07 -0600 Subject: [PATCH 35/48] Fix up error messages --- tools/GitHubActions.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/GitHubActions.ps1 b/tools/GitHubActions.ps1 index fbe5439..a2a04c5 100644 --- a/tools/GitHubActions.ps1 +++ b/tools/GitHubActions.ps1 @@ -9,11 +9,11 @@ function Add-GitHubActionsEnvVariable { ) if ([string]::IsNullOrWhiteSpace($Path)) { - throw "GitHub Actions environment file must not be empty." + throw "GitHub Actions environment file path must not be empty." } if ([string]::IsNullOrWhiteSpace($Name)) { - throw "GitHub Actions environment variable names must not be empty." + throw "GitHub Actions environment variable name must not be empty." } $utf8NoBom = [System.Text.UTF8Encoding]::new($false) @@ -29,11 +29,11 @@ function Add-GitHubActionsPath { ) if ([string]::IsNullOrWhiteSpace($Path)) { - throw "GitHub Actions path file must not be empty." + throw "GitHub Actions path file path must not be empty." } if ([string]::IsNullOrWhiteSpace($Value)) { - throw "GitHub Actions path entries must not be empty." + throw "GitHub Actions path entry must not be empty." } $utf8NoBom = [System.Text.UTF8Encoding]::new($false) From 8201f772e034b548f6fef19445abf5495235594e Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 09:18:58 -0600 Subject: [PATCH 36/48] Avoid importing GitHubActions.ps1 outside GitHub Actions --- tools/Set-EnvVars.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1 index c2d95b8..c85d694 100644 --- a/tools/Set-EnvVars.ps1 +++ b/tools/Set-EnvVars.ps1 @@ -19,7 +19,9 @@ Param( [string[]]$PrependPath ) -. "$PSScriptRoot\GitHubActions.ps1" +if ($env:GITHUB_ACTIONS) { + . "$PSScriptRoot\GitHubActions.ps1" +} if ($Variables.Count -eq 0) { return $true From 3ee5eeb3ffaad4da5b5c474d46231033bd64a6df Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 09:20:03 -0600 Subject: [PATCH 37/48] Clarify yml comment --- azure-pipelines/prepare-insertion-stages.yml | 2 +- azure-pipelines/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines/prepare-insertion-stages.yml b/azure-pipelines/prepare-insertion-stages.yml index d772332..9abd377 100644 --- a/azure-pipelines/prepare-insertion-stages.yml +++ b/azure-pipelines/prepare-insertion-stages.yml @@ -51,7 +51,7 @@ stages: pool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary + - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade this job to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary os: Linux templateContext: outputs: diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index dda671c..a8218f0 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -33,7 +33,7 @@ extends: pool: name: AzurePipelines-EO demands: - - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary + - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade this job to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary os: Linux templateContext: outputs: From 3948cdc9c039c4915500da6c63d9d3bd0ac7a40f Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 09:21:17 -0600 Subject: [PATCH 38/48] Update script synopsis --- tools/variables/_define.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/variables/_define.ps1 b/tools/variables/_define.ps1 index 90fd080..ba081f0 100644 --- a/tools/variables/_define.ps1 +++ b/tools/variables/_define.ps1 @@ -1,7 +1,7 @@ <# .SYNOPSIS This script translates the variables returned by the _all.ps1 script - into commands that instruct Azure Pipelines to actually set those variables for other pipeline tasks to consume. + into commands that instruct Azure Pipelines or GitHub Actions to actually set those variables for other pipeline tasks to consume. The build or release definition may have set these variables to override what the build would do. So only set them if they have not already been set. From 6a14f7669346ca5247dfb0666586f9a100bdeb5a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 09:45:21 -0600 Subject: [PATCH 39/48] Clarify what looks like bad wording --- tools/GitHubActions.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/GitHubActions.ps1 b/tools/GitHubActions.ps1 index a2a04c5..c732a0f 100644 --- a/tools/GitHubActions.ps1 +++ b/tools/GitHubActions.ps1 @@ -9,7 +9,7 @@ function Add-GitHubActionsEnvVariable { ) if ([string]::IsNullOrWhiteSpace($Path)) { - throw "GitHub Actions environment file path must not be empty." + throw "GitHub Actions GITHUB_ENV file path must not be empty." } if ([string]::IsNullOrWhiteSpace($Name)) { @@ -29,7 +29,7 @@ function Add-GitHubActionsPath { ) if ([string]::IsNullOrWhiteSpace($Path)) { - throw "GitHub Actions path file path must not be empty." + throw "GitHub Actions GITHUB_PATH file path must not be empty." } if ([string]::IsNullOrWhiteSpace($Value)) { From c456c941b5df24d5eb6184fa5033a139abf29e9a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:06:22 -0600 Subject: [PATCH 40/48] Update nbgv and nerdbank.gitversioning updates to 3.10.91 (538) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- Directory.Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9484456..30cea5a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "nbgv": { - "version": "3.10.85", + "version": "3.10.91", "commands": [ "nbgv" ], diff --git a/Directory.Packages.props b/Directory.Packages.props index 2e095f2..ca1e17d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - + From 036db57c0053f15d56761acb53fb01cd2f331ccb Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 19:23:23 -0600 Subject: [PATCH 41/48] Cache the dotnet-install scripts locally This should avoid the failures we're seeing in azure pipelines from github throttling unauthenticated requests. --- tools/Install-DotNetSdk.ps1 | 13 +- tools/Update-DotNetInstallScript.ps1 | 36 + tools/dotnet-install.ps1 | 1356 ++++++++++++++++++ tools/dotnet-install.sh | 1888 ++++++++++++++++++++++++++ 4 files changed, 3285 insertions(+), 8 deletions(-) create mode 100644 tools/Update-DotNetInstallScript.ps1 create mode 100644 tools/dotnet-install.ps1 create mode 100644 tools/dotnet-install.sh diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1 index d9d35bb..bec37c9 100644 --- a/tools/Install-DotNetSdk.ps1 +++ b/tools/Install-DotNetSdk.ps1 @@ -284,18 +284,15 @@ if ($IncludeX86) { } if ($IsMacOS -or $IsLinux) { - $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/a3fbd0fd625032bac207f1f590e5353fe26faa59/src/dotnet-install.sh" - $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.sh" + $DotNetInstallScriptPath = "$PSScriptRoot/dotnet-install.sh" } else { - $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/a3fbd0fd625032bac207f1f590e5353fe26faa59/src/dotnet-install.ps1" - $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.ps1" + $DotNetInstallScriptPath = "$PSScriptRoot/dotnet-install.ps1" } +# Verify the cached script exists if (-not (Test-Path $DotNetInstallScriptPath)) { - Invoke-WebRequest -Uri $DownloadUri -OutFile $DotNetInstallScriptPath -UseBasicParsing - if ($IsMacOS -or $IsLinux) { - chmod +x $DotNetInstallScriptPath - } + Write-Error "Cached dotnet-install script not found at $DotNetInstallScriptPath. Run tools/Update-DotNetInstallScript.ps1 to download it." + exit 1 } # In case the script we invoke is in a directory with spaces, wrap it with single quotes. diff --git a/tools/Update-DotNetInstallScript.ps1 b/tools/Update-DotNetInstallScript.ps1 new file mode 100644 index 0000000..bc0cba6 --- /dev/null +++ b/tools/Update-DotNetInstallScript.ps1 @@ -0,0 +1,36 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Updates the cached dotnet-install scripts from the dotnet/install-scripts GitHub repository. +.DESCRIPTION + Downloads the latest dotnet-install.ps1 and dotnet-install.sh scripts from + https://github.com/dotnet/install-scripts and caches them locally to avoid GitHub API rate limiting. + Run this script periodically to get the latest installation scripts. +#> +[CmdletBinding(SupportsShouldProcess = $true)] +Param() + +$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$DownloadBaseUri = "https://raw.githubusercontent.com/dotnet/install-scripts/main/src" + +$scripts = @('dotnet-install.ps1', 'dotnet-install.sh') + +foreach ($script in $scripts) { + $Uri = "$DownloadBaseUri/$script" + $OutFile = Join-Path $ScriptRoot $script + + Write-Host "Updating $script from GitHub..." + try { + if ($PSCmdlet.ShouldProcess($OutFile, "Update from $Uri")) { + Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing + Write-Host "โœ“ Successfully updated $script" -ForegroundColor Green + } + } + catch { + Write-Error "Failed to update ${script}: $_" + exit 1 + } +} + +Write-Host "All cached scripts have been updated." -ForegroundColor Green diff --git a/tools/dotnet-install.ps1 b/tools/dotnet-install.ps1 new file mode 100644 index 0000000..e62264b --- /dev/null +++ b/tools/dotnet-install.ps1 @@ -0,0 +1,1356 @@ +# +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +<# +.SYNOPSIS + Installs dotnet cli +.DESCRIPTION + Installs dotnet cli. If dotnet installation already exists in the given directory + it will update it only if the requested version differs from the one already installed. + + Note that the intended use of this script is for Continuous Integration (CI) scenarios, where: + - The SDK needs to be installed without user interaction and without admin rights. + - The SDK installation doesn't need to persist across multiple CI runs. + To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer. + +.PARAMETER Channel + Default: LTS + Download from the Channel specified. Possible values: + - STS - the most recent Standard Term Support release + - LTS - the most recent Long Term Support release + - 2-part version in a format A.B - represents a specific release + examples: 2.0, 1.0 + - 3-part version in a format A.B.Cxx - represents a specific SDK release + examples: 5.0.1xx, 5.0.2xx + Supported since 5.0 release + Warning: Value "Current" is deprecated for the Channel parameter. Use "STS" instead. + Note: The version parameter overrides the channel parameter when any version other than 'latest' is used. +.PARAMETER Quality + Download the latest build of specified quality in the channel. The possible values are: daily, preview, GA. + Works only in combination with channel. Not applicable for STS and LTS channels and will be ignored if those channels are used. + For SDK use channel in A.B.Cxx format: using quality together with channel in A.B format is not supported. + Supported since 5.0 release. + Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality. +.PARAMETER Version + Default: latest + Represents a build version on specific channel. Possible values: + - latest - the latest build on specific channel + - 3-part version in a format A.B.C - represents specific version of build + examples: 2.0.0-preview2-006120, 1.1.0 +.PARAMETER Internal + Download internal builds. Requires providing credentials via -FeedCredential parameter. +.PARAMETER FeedCredential + Token to access Azure feed. Used as a query string to append to the Azure feed. + This parameter typically is not specified. +.PARAMETER InstallDir + Default: %LocalAppData%\Microsoft\dotnet + Path to where to install dotnet. Note that binaries will be placed directly in a given directory. +.PARAMETER Architecture + Default: - this value represents currently running OS architecture + Architecture of dotnet binaries to be installed. + Possible values are: , amd64, x64, x86, arm64, arm +.PARAMETER SharedRuntime + This parameter is obsolete and may be removed in a future version of this script. + The recommended alternative is '-Runtime dotnet'. + Installs just the shared runtime bits, not the entire SDK. +.PARAMETER Runtime + Installs just a shared runtime, not the entire SDK. + Possible values: + - dotnet - the Microsoft.NETCore.App shared runtime + - aspnetcore - the Microsoft.AspNetCore.App shared runtime + - windowsdesktop - the Microsoft.WindowsDesktop.App shared runtime +.PARAMETER DryRun + If set it will not perform installation but instead display what command line to use to consistently install + currently requested version of dotnet cli. In example if you specify version 'latest' it will display a link + with specific version so that this command can be used deterministically in a build script. + It also displays binaries location if you prefer to install or download it yourself. +.PARAMETER NoPath + By default this script will set environment variable PATH for the current process to the binaries folder inside installation folder. + If set it will display binaries location but not set any environment variable. +.PARAMETER Verbose + Displays diagnostics information. +.PARAMETER AzureFeed + Default: https://builds.dotnet.microsoft.com/dotnet + For internal use only. + Allows using a different storage to download SDK archives from. +.PARAMETER UncachedFeed + For internal use only. + Allows using a different storage to download SDK archives from. +.PARAMETER ProxyAddress + If set, the installer will use the proxy when making web requests +.PARAMETER ProxyUseDefaultCredentials + Default: false + Use default credentials, when using proxy address. +.PARAMETER ProxyBypassList + If set with ProxyAddress, will provide the list of comma separated urls that will bypass the proxy +.PARAMETER SkipNonVersionedFiles + Default: false + Skips installing non-versioned files if they already exist, such as dotnet.exe. +.PARAMETER JSonFile + Determines the SDK version from a user specified global.json file + Note: global.json must have a value for 'SDK:Version' +.PARAMETER DownloadTimeout + Determines timeout duration in seconds for downloading of the SDK file + Default: 1200 seconds (20 minutes) +.PARAMETER KeepZip + If set, downloaded file is kept +.PARAMETER ZipPath + Use that path to store installer, generated by default +.EXAMPLE + dotnet-install.ps1 -Version 7.0.401 + Installs the .NET SDK version 7.0.401 +.EXAMPLE + dotnet-install.ps1 -Channel 8.0 -Quality GA + Installs the latest GA (general availability) version of the .NET 8.0 SDK +#> +[cmdletbinding()] +param( + [string]$Channel = "LTS", + [string]$Quality, + [string]$Version = "Latest", + [switch]$Internal, + [string]$JSonFile, + [Alias('i')][string]$InstallDir = "", + [string]$Architecture = "", + [string]$Runtime, + [Obsolete("This parameter may be removed in a future version of this script. The recommended alternative is '-Runtime dotnet'.")] + [switch]$SharedRuntime, + [switch]$DryRun, + [switch]$NoPath, + [string]$AzureFeed, + [string]$UncachedFeed, + [string]$FeedCredential, + [string]$ProxyAddress, + [switch]$ProxyUseDefaultCredentials, + [string[]]$ProxyBypassList = @(), + [switch]$SkipNonVersionedFiles, + [int]$DownloadTimeout = 1200, + [switch]$KeepZip, + [string]$ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()), + [switch]$Help +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +function Say($str) { + try { + Write-Host "dotnet-install: $str" + } + catch { + # Some platforms cannot utilize Write-Host (Azure Functions, for instance). Fall back to Write-Output + Write-Output "dotnet-install: $str" + } +} + +function Say-Warning($str) { + try { + Write-Warning "dotnet-install: $str" + } + catch { + # Some platforms cannot utilize Write-Warning (Azure Functions, for instance). Fall back to Write-Output + Write-Output "dotnet-install: Warning: $str" + } +} + +# Writes a line with error style settings. +# Use this function to show a human-readable comment along with an exception. +function Say-Error($str) { + try { + # Write-Error is quite verbose for the purpose of the function, let's write one line with error style settings. + $Host.UI.WriteErrorLine("dotnet-install: $str") + } + catch { + Write-Output "dotnet-install: Error: $str" + } +} + +function Say-Verbose($str) { + try { + Write-Verbose "dotnet-install: $str" + } + catch { + # Some platforms cannot utilize Write-Verbose (Azure Functions, for instance). Fall back to Write-Output + Write-Output "dotnet-install: $str" + } +} + +function Measure-Action($name, $block) { + $time = Measure-Command $block + $totalSeconds = $time.TotalSeconds + Say-Verbose "Action '$name' took $totalSeconds seconds" +} + +function Get-Remote-File-Size($zipUri) { + try { + $response = Invoke-WebRequest -Uri $zipUri -Method Head + $fileSize = $response.Headers["Content-Length"] + if ((![string]::IsNullOrEmpty($fileSize))) { + Say "Remote file $zipUri size is $fileSize bytes." + + return $fileSize + } + } + catch { + Say-Verbose "Content-Length header was not extracted for $zipUri." + } + + return $null +} + +function Say-Invocation($Invocation) { + $command = $Invocation.MyCommand; + $args = (($Invocation.BoundParameters.Keys | foreach { "-$_ `"$($Invocation.BoundParameters[$_])`"" }) -join " ") + Say-Verbose "$command $args" +} + +function Invoke-With-Retry([ScriptBlock]$ScriptBlock, [System.Threading.CancellationToken]$cancellationToken = [System.Threading.CancellationToken]::None, [int]$MaxAttempts = 3, [int]$SecondsBetweenAttempts = 1) { + $Attempts = 0 + $local:startTime = $(get-date) + + while ($true) { + try { + return & $ScriptBlock + } + catch { + $Attempts++ + if (($Attempts -lt $MaxAttempts) -and -not $cancellationToken.IsCancellationRequested) { + Start-Sleep $SecondsBetweenAttempts + } + else { + $local:elapsedTime = $(get-date) - $local:startTime + if (($local:elapsedTime.TotalSeconds - $DownloadTimeout) -gt 0 -and -not $cancellationToken.IsCancellationRequested) { + throw New-Object System.TimeoutException("Failed to reach the server: connection timeout: default timeout is $DownloadTimeout second(s)"); + } + throw; + } + } + } +} + +function Get-Machine-Architecture() { + Say-Invocation $MyInvocation + + # On PS x86, PROCESSOR_ARCHITECTURE reports x86 even on x64 systems. + # To get the correct architecture, we need to use PROCESSOR_ARCHITEW6432. + # PS x64 doesn't define this, so we fall back to PROCESSOR_ARCHITECTURE. + # Possible values: amd64, x64, x86, arm64, arm + if ( $ENV:PROCESSOR_ARCHITEW6432 -ne $null ) { + return $ENV:PROCESSOR_ARCHITEW6432 + } + + try { + if ( ((Get-CimInstance -ClassName CIM_OperatingSystem).OSArchitecture) -like "ARM*") { + if ( [Environment]::Is64BitOperatingSystem ) { + return "arm64" + } + return "arm" + } + } + catch { + # Machine doesn't support Get-CimInstance + } + + return $ENV:PROCESSOR_ARCHITECTURE +} + +function Get-CLIArchitecture-From-Architecture([string]$Architecture) { + Say-Invocation $MyInvocation + + if ($Architecture -eq "") { + $Architecture = Get-Machine-Architecture + } + + switch ($Architecture.ToLowerInvariant()) { + { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } + { $_ -eq "x86" } { return "x86" } + { $_ -eq "arm" } { return "arm" } + { $_ -eq "arm64" } { return "arm64" } + default { throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" } + } +} + +function ValidateFeedCredential([string] $FeedCredential) { + if ($Internal -and [string]::IsNullOrWhitespace($FeedCredential)) { + $message = "Provide credentials via -FeedCredential parameter." + if ($DryRun) { + Say-Warning "$message" + } + else { + throw "$message" + } + } + + #FeedCredential should start with "?", for it to be added to the end of the link. + #adding "?" at the beginning of the FeedCredential if needed. + if ((![string]::IsNullOrWhitespace($FeedCredential)) -and ($FeedCredential[0] -ne '?')) { + $FeedCredential = "?" + $FeedCredential + } + + return $FeedCredential +} +function Get-NormalizedQuality([string]$Quality) { + Say-Invocation $MyInvocation + + if ([string]::IsNullOrEmpty($Quality)) { + return "" + } + + switch ($Quality) { + { @("daily", "preview") -contains $_ } { return $Quality.ToLowerInvariant() } + #ga quality is available without specifying quality, so normalizing it to empty + { $_ -eq "ga" } { return "" } + default { throw "'$Quality' is not a supported value for -Quality option. Supported values are: daily, preview, ga. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." } + } +} + +function Get-NormalizedChannel([string]$Channel) { + Say-Invocation $MyInvocation + + if ([string]::IsNullOrEmpty($Channel)) { + return "" + } + + if ($Channel.Contains("Current")) { + Say-Warning 'Value "Current" is deprecated for -Channel option. Use "STS" instead.' + } + + if ($Channel.StartsWith('release/')) { + Say-Warning 'Using branch name with -Channel option is no longer supported with newer releases. Use -Quality option with a channel in X.Y format instead, such as "-Channel 5.0 -Quality Daily."' + } + + switch ($Channel) { + { $_ -eq "lts" } { return "LTS" } + { $_ -eq "sts" } { return "STS" } + { $_ -eq "current" } { return "STS" } + default { return $Channel.ToLowerInvariant() } + } +} + +function Get-NormalizedProduct([string]$Runtime) { + Say-Invocation $MyInvocation + + switch ($Runtime) { + { $_ -eq "dotnet" } { return "dotnet-runtime" } + { $_ -eq "aspnetcore" } { return "aspnetcore-runtime" } + { $_ -eq "windowsdesktop" } { return "windowsdesktop-runtime" } + { [string]::IsNullOrEmpty($_) } { return "dotnet-sdk" } + default { throw "'$Runtime' is not a supported value for -Runtime option, supported values are: dotnet, aspnetcore, windowsdesktop. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." } + } +} + + +# The version text returned from the feeds is a 1-line or 2-line string: +# For the SDK and the dotnet runtime (2 lines): +# Line 1: # commit_hash +# Line 2: # 4-part version +# For the aspnetcore runtime (1 line): +# Line 1: # 4-part version +function Get-Version-From-LatestVersion-File-Content([string]$VersionText) { + Say-Invocation $MyInvocation + + $Data = -split $VersionText + + $VersionInfo = @{ + CommitHash = $(if ($Data.Count -gt 1) { $Data[0] }) + Version = $Data[-1] # last line is always the version number. + } + return $VersionInfo +} + +function Load-Assembly([string] $Assembly) { + try { + Add-Type -Assembly $Assembly | Out-Null + } + catch { + # On Nano Server, Powershell Core Edition is used. Add-Type is unable to resolve base class assemblies because they are not GAC'd. + # Loading the base class assemblies is not unnecessary as the types will automatically get resolved. + } +} + +function GetHTTPResponse([Uri] $Uri, [bool]$HeaderOnly, [bool]$DisableRedirect, [bool]$DisableFeedCredential) { + $cts = New-Object System.Threading.CancellationTokenSource + + $downloadScript = { + + $HttpClient = $null + + try { + # HttpClient is used vs Invoke-WebRequest in order to support Nano Server which doesn't support the Invoke-WebRequest cmdlet. + Load-Assembly -Assembly System.Net.Http + + if (-not $ProxyAddress) { + try { + # Despite no proxy being explicitly specified, we may still be behind a default proxy + $DefaultProxy = [System.Net.WebRequest]::DefaultWebProxy; + if ($DefaultProxy -and (-not $DefaultProxy.IsBypassed($Uri))) { + if ($null -ne $DefaultProxy.GetProxy($Uri)) { + $ProxyAddress = $DefaultProxy.GetProxy($Uri).OriginalString + } + else { + $ProxyAddress = $null + } + $ProxyUseDefaultCredentials = $true + } + } + catch { + # Eat the exception and move forward as the above code is an attempt + # at resolving the DefaultProxy that may not have been a problem. + $ProxyAddress = $null + Say-Verbose("Exception ignored: $_.Exception.Message - moving forward...") + } + } + + $HttpClientHandler = New-Object System.Net.Http.HttpClientHandler + if ($ProxyAddress) { + $HttpClientHandler.Proxy = New-Object System.Net.WebProxy -Property @{ + Address = $ProxyAddress; + UseDefaultCredentials = $ProxyUseDefaultCredentials; + BypassList = $ProxyBypassList; + } + } + if ($DisableRedirect) { + $HttpClientHandler.AllowAutoRedirect = $false + } + $HttpClient = New-Object System.Net.Http.HttpClient -ArgumentList $HttpClientHandler + + # Default timeout for HttpClient is 100s. For a 50 MB download this assumes 500 KB/s average, any less will time out + # Defaulting to 20 minutes allows it to work over much slower connections. + $HttpClient.Timeout = New-TimeSpan -Seconds $DownloadTimeout + + if ($HeaderOnly) { + $completionOption = [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead + } + else { + $completionOption = [System.Net.Http.HttpCompletionOption]::ResponseContentRead + } + + if ($DisableFeedCredential) { + $UriWithCredential = $Uri + } + else { + $UriWithCredential = "${Uri}${FeedCredential}" + } + + $Task = $HttpClient.GetAsync("$UriWithCredential", $completionOption).ConfigureAwait("false"); + $Response = $Task.GetAwaiter().GetResult(); + + if (($null -eq $Response) -or ((-not $HeaderOnly) -and (-not ($Response.IsSuccessStatusCode)))) { + # The feed credential is potentially sensitive info. Do not log FeedCredential to console output. + $DownloadException = [System.Exception] "Unable to download $Uri." + + if ($null -ne $Response) { + $DownloadException.Data["StatusCode"] = [int] $Response.StatusCode + $DownloadException.Data["ErrorMessage"] = "Unable to download $Uri. Returned HTTP status code: " + $DownloadException.Data["StatusCode"] + + if (404 -eq [int] $Response.StatusCode) { + $cts.Cancel() + } + } + + throw $DownloadException + } + + return $Response + } + catch [System.Net.Http.HttpRequestException] { + $DownloadException = [System.Exception] "Unable to download $Uri." + + # Pick up the exception message and inner exceptions' messages if they exist + $CurrentException = $PSItem.Exception + $ErrorMsg = $CurrentException.Message + "`r`n" + while ($CurrentException.InnerException) { + $CurrentException = $CurrentException.InnerException + $ErrorMsg += $CurrentException.Message + "`r`n" + } + + # Check if there is an issue concerning TLS. + if ($ErrorMsg -like "*SSL/TLS*") { + $ErrorMsg += "Ensure that TLS 1.2 or higher is enabled to use this script.`r`n" + } + + $DownloadException.Data["ErrorMessage"] = $ErrorMsg + throw $DownloadException + } + finally { + if ($null -ne $HttpClient) { + $HttpClient.Dispose() + } + } + } + + try { + return Invoke-With-Retry $downloadScript $cts.Token + } + finally { + if ($null -ne $cts) { + $cts.Dispose() + } + } +} + +function Get-Version-From-LatestVersion-File([string]$AzureFeed, [string]$Channel) { + Say-Invocation $MyInvocation + + $VersionFileUrl = $null + if ($Runtime -eq "dotnet") { + $VersionFileUrl = "$AzureFeed/Runtime/$Channel/latest.version" + } + elseif ($Runtime -eq "aspnetcore") { + $VersionFileUrl = "$AzureFeed/aspnetcore/Runtime/$Channel/latest.version" + } + elseif ($Runtime -eq "windowsdesktop") { + $VersionFileUrl = "$AzureFeed/WindowsDesktop/$Channel/latest.version" + } + elseif (-not $Runtime) { + $VersionFileUrl = "$AzureFeed/Sdk/$Channel/latest.version" + } + else { + throw "Invalid value for `$Runtime" + } + + Say-Verbose "Constructed latest.version URL: $VersionFileUrl" + + try { + $Response = GetHTTPResponse -Uri $VersionFileUrl + } + catch { + Say-Verbose "Failed to download latest.version file." + throw + } + $StringContent = $Response.Content.ReadAsStringAsync().Result + + switch ($Response.Content.Headers.ContentType) { + { ($_ -eq "application/octet-stream") } { $VersionText = $StringContent } + { ($_ -eq "text/plain") } { $VersionText = $StringContent } + { ($_ -eq "text/plain; charset=UTF-8") } { $VersionText = $StringContent } + default { throw "``$Response.Content.Headers.ContentType`` is an unknown .version file content type." } + } + + $VersionInfo = Get-Version-From-LatestVersion-File-Content $VersionText + + return $VersionInfo +} + +function Parse-Jsonfile-For-Version([string]$JSonFile) { + Say-Invocation $MyInvocation + + If (-Not (Test-Path $JSonFile)) { + throw "Unable to find '$JSonFile'" + } + try { + $JSonContent = Get-Content($JSonFile) -Raw | ConvertFrom-Json | Select-Object -expand "sdk" -ErrorAction SilentlyContinue + } + catch { + Say-Error "Json file unreadable: '$JSonFile'" + throw + } + if ($JSonContent) { + try { + $JSonContent.PSObject.Properties | ForEach-Object { + $PropertyName = $_.Name + if ($PropertyName -eq "version") { + $Version = $_.Value + Say-Verbose "Version = $Version" + } + } + } + catch { + Say-Error "Unable to parse the SDK node in '$JSonFile'" + throw + } + } + else { + throw "Unable to find the SDK node in '$JSonFile'" + } + If ($Version -eq $null) { + throw "Unable to find the SDK:version node in '$JSonFile'" + } + return $Version +} + +function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel, [string]$Version, [string]$JSonFile) { + Say-Invocation $MyInvocation + + if (-not $JSonFile) { + if ($Version.ToLowerInvariant() -eq "latest") { + $LatestVersionInfo = Get-Version-From-LatestVersion-File -AzureFeed $AzureFeed -Channel $Channel + return $LatestVersionInfo.Version + } + else { + return $Version + } + } + else { + return Parse-Jsonfile-For-Version $JSonFile + } +} + +function Get-Download-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + # If anything fails in this lookup it will default to $SpecificVersion + $SpecificProductVersion = Get-Product-Version -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion + + if ($Runtime -eq "dotnet") { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + } + elseif ($Runtime -eq "aspnetcore") { + $PayloadURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + } + elseif ($Runtime -eq "windowsdesktop") { + # The windows desktop runtime is part of the core runtime layout prior to 5.0 + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + if ($SpecificVersion -match '^(\d+)\.(.*)$') { + $majorVersion = [int]$Matches[1] + if ($majorVersion -ge 5) { + $PayloadURL = "$AzureFeed/WindowsDesktop/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + } + } + } + elseif (-not $Runtime) { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificProductVersion-win-$CLIArchitecture.zip" + } + else { + throw "Invalid value for `$Runtime" + } + + Say-Verbose "Constructed primary named payload URL: $PayloadURL" + + return $PayloadURL, $SpecificProductVersion +} + +function Get-LegacyDownload-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + if (-not $Runtime) { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-dev-win-$CLIArchitecture.$SpecificVersion.zip" + } + elseif ($Runtime -eq "dotnet") { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-win-$CLIArchitecture.$SpecificVersion.zip" + } + else { + return $null + } + + Say-Verbose "Constructed legacy named payload URL: $PayloadURL" + + return $PayloadURL +} + +function Get-Product-Version([string]$AzureFeed, [string]$SpecificVersion, [string]$PackageDownloadLink) { + Say-Invocation $MyInvocation + + # Try to get the version number, using the productVersion.txt file located next to the installer file. + $ProductVersionTxtURLs = (Get-Product-Version-Url $AzureFeed $SpecificVersion $PackageDownloadLink -Flattened $true), + (Get-Product-Version-Url $AzureFeed $SpecificVersion $PackageDownloadLink -Flattened $false) + + Foreach ($ProductVersionTxtURL in $ProductVersionTxtURLs) { + Say-Verbose "Checking for the existence of $ProductVersionTxtURL" + + try { + $productVersionResponse = GetHTTPResponse($productVersionTxtUrl) + + if ($productVersionResponse.StatusCode -eq 200) { + $productVersion = $productVersionResponse.Content.ReadAsStringAsync().Result.Trim() + if ($productVersion -ne $SpecificVersion) { + Say "Using alternate version $productVersion found in $ProductVersionTxtURL" + } + return $productVersion + } + else { + Say-Verbose "Got StatusCode $($productVersionResponse.StatusCode) when trying to get productVersion.txt at $productVersionTxtUrl." + } + } + catch { + Say-Verbose "Could not read productVersion.txt at $productVersionTxtUrl (Exception: '$($_.Exception.Message)'. )" + } + } + + # Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number. + if ([string]::IsNullOrEmpty($PackageDownloadLink)) { + Say-Verbose "Using the default value '$SpecificVersion' as the product version." + return $SpecificVersion + } + + $productVersion = Get-ProductVersionFromDownloadLink $PackageDownloadLink $SpecificVersion + return $productVersion +} + +function Get-Product-Version-Url([string]$AzureFeed, [string]$SpecificVersion, [string]$PackageDownloadLink, [bool]$Flattened) { + Say-Invocation $MyInvocation + + $majorVersion = $null + if ($SpecificVersion -match '^(\d+)\.(.*)') { + $majorVersion = $Matches[1] -as [int] + } + + $pvFileName = 'productVersion.txt' + if ($Flattened) { + if (-not $Runtime) { + $pvFileName = 'sdk-productVersion.txt' + } + elseif ($Runtime -eq "dotnet") { + $pvFileName = 'runtime-productVersion.txt' + } + else { + $pvFileName = "$Runtime-productVersion.txt" + } + } + + if ([string]::IsNullOrEmpty($PackageDownloadLink)) { + if ($Runtime -eq "dotnet") { + $ProductVersionTxtURL = "$AzureFeed/Runtime/$SpecificVersion/$pvFileName" + } + elseif ($Runtime -eq "aspnetcore") { + $ProductVersionTxtURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/$pvFileName" + } + elseif ($Runtime -eq "windowsdesktop") { + # The windows desktop runtime is part of the core runtime layout prior to 5.0 + $ProductVersionTxtURL = "$AzureFeed/Runtime/$SpecificVersion/$pvFileName" + if ($majorVersion -ne $null -and $majorVersion -ge 5) { + $ProductVersionTxtURL = "$AzureFeed/WindowsDesktop/$SpecificVersion/$pvFileName" + } + } + elseif (-not $Runtime) { + $ProductVersionTxtURL = "$AzureFeed/Sdk/$SpecificVersion/$pvFileName" + } + else { + throw "Invalid value '$Runtime' specified for `$Runtime" + } + } + else { + $ProductVersionTxtURL = $PackageDownloadLink.Substring(0, $PackageDownloadLink.LastIndexOf("/")) + "/$pvFileName" + } + + Say-Verbose "Constructed productVersion link: $ProductVersionTxtURL" + + return $ProductVersionTxtURL +} + +function Get-ProductVersionFromDownloadLink([string]$PackageDownloadLink, [string]$SpecificVersion) { + Say-Invocation $MyInvocation + + #product specific version follows the product name + #for filename 'dotnet-sdk-3.1.404-win-x64.zip': the product version is 3.1.400 + $filename = $PackageDownloadLink.Substring($PackageDownloadLink.LastIndexOf("/") + 1) + $filenameParts = $filename.Split('-') + if ($filenameParts.Length -gt 2) { + $productVersion = $filenameParts[2] + Say-Verbose "Extracted product version '$productVersion' from download link '$PackageDownloadLink'." + } + else { + Say-Verbose "Using the default value '$SpecificVersion' as the product version." + $productVersion = $SpecificVersion + } + return $productVersion +} + +function Get-User-Share-Path() { + Say-Invocation $MyInvocation + + $InstallRoot = $env:DOTNET_INSTALL_DIR + if (!$InstallRoot) { + $InstallRoot = "$env:LocalAppData\Microsoft\dotnet" + } + elseif ($InstallRoot -like "$env:ProgramFiles\dotnet\?*") { + Say-Warning "The install root specified by the environment variable DOTNET_INSTALL_DIR points to the sub folder of $env:ProgramFiles\dotnet which is the default dotnet install root using .NET SDK installer. It is better to keep aligned with .NET SDK installer." + } + return $InstallRoot +} + +function Resolve-Installation-Path([string]$InstallDir) { + Say-Invocation $MyInvocation + + if ($InstallDir -eq "") { + return Get-User-Share-Path + } + return $InstallDir +} + +function Test-User-Write-Access([string]$InstallDir) { + try { + $tempFileName = [guid]::NewGuid().ToString() + $tempFilePath = Join-Path -Path $InstallDir -ChildPath $tempFileName + New-Item -Path $tempFilePath -ItemType File -Force + Remove-Item $tempFilePath -Force + return $true + } + catch { + return $false + } +} + +function Is-Dotnet-Package-Installed([string]$InstallRoot, [string]$RelativePathToPackage, [string]$SpecificVersion) { + Say-Invocation $MyInvocation + + $DotnetPackagePath = Join-Path -Path $InstallRoot -ChildPath $RelativePathToPackage | Join-Path -ChildPath $SpecificVersion + Say-Verbose "Is-Dotnet-Package-Installed: DotnetPackagePath=$DotnetPackagePath" + return Test-Path $DotnetPackagePath -PathType Container +} + +function Get-Absolute-Path([string]$RelativeOrAbsolutePath) { + # Too much spam + # Say-Invocation $MyInvocation + + return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($RelativeOrAbsolutePath) +} + +function Get-Path-Prefix-With-Version($path) { + # example path with regex: shared/1.0.0-beta-12345/somepath + $match = [regex]::match($path, "/\d+\.\d+[^/]+/") + if ($match.Success) { + return $entry.FullName.Substring(0, $match.Index + $match.Length) + } + + return $null +} + +function Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package([System.IO.Compression.ZipArchive]$Zip, [string]$OutPath) { + Say-Invocation $MyInvocation + + $ret = @() + foreach ($entry in $Zip.Entries) { + $dir = Get-Path-Prefix-With-Version $entry.FullName + if ($null -ne $dir) { + $path = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $dir) + if (-Not (Test-Path $path -PathType Container)) { + $ret += $dir + } + } + } + + $ret = $ret | Sort-Object | Get-Unique + + $values = ($ret | foreach { "$_" }) -join ";" + Say-Verbose "Directories to unpack: $values" + + return $ret +} + +# Example zip content and extraction algorithm: +# Rule: files if extracted are always being extracted to the same relative path locally +# .\ +# a.exe # file does not exist locally, extract +# b.dll # file exists locally, override only if $OverrideFiles set +# aaa\ # same rules as for files +# ... +# abc\1.0.0\ # directory contains version and exists locally +# ... # do not extract content under versioned part +# abc\asd\ # same rules as for files +# ... +# def\ghi\1.0.1\ # directory contains version and does not exist locally +# ... # extract content +function Extract-Dotnet-Package([string]$ZipPath, [string]$OutPath) { + Say-Invocation $MyInvocation + + Load-Assembly -Assembly System.IO.Compression.FileSystem + Set-Variable -Name Zip + try { + $Zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) + + $DirectoriesToUnpack = Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package -Zip $Zip -OutPath $OutPath + + foreach ($entry in $Zip.Entries) { + $PathWithVersion = Get-Path-Prefix-With-Version $entry.FullName + if (($null -eq $PathWithVersion) -Or ($DirectoriesToUnpack -contains $PathWithVersion)) { + $DestinationPath = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $entry.FullName) + $DestinationDir = Split-Path -Parent $DestinationPath + $OverrideFiles = $OverrideNonVersionedFiles -Or (-Not (Test-Path $DestinationPath)) + if ((-Not $DestinationPath.EndsWith("\")) -And $OverrideFiles) { + New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $DestinationPath, $OverrideNonVersionedFiles) + } + } + } + } + catch { + Say-Error "Failed to extract package. Exception: $_" + throw; + } + finally { + if ($null -ne $Zip) { + $Zip.Dispose() + } + } +} + +function DownloadFile($Source, [string]$OutPath) { + if ($Source -notlike "http*") { + # Using System.IO.Path.GetFullPath to get the current directory + # does not work in this context - $pwd gives the current directory + if (![System.IO.Path]::IsPathRooted($Source)) { + $Source = $(Join-Path -Path $pwd -ChildPath $Source) + } + $Source = Get-Absolute-Path $Source + Say "Copying file from $Source to $OutPath" + Copy-Item $Source $OutPath + return + } + + $Stream = $null + + try { + $Response = GetHTTPResponse -Uri $Source + $Stream = $Response.Content.ReadAsStreamAsync().Result + $File = [System.IO.File]::Create($OutPath) + $Stream.CopyTo($File) + $File.Close() + + ValidateRemoteLocalFileSizes -LocalFileOutPath $OutPath -SourceUri $Source + } + finally { + if ($null -ne $Stream) { + $Stream.Dispose() + } + } +} + +function ValidateRemoteLocalFileSizes([string]$LocalFileOutPath, $SourceUri) { + try { + $remoteFileSize = Get-Remote-File-Size -zipUri $SourceUri + $fileSize = [long](Get-Item $LocalFileOutPath).Length + Say "Downloaded file $SourceUri size is $fileSize bytes." + + if ((![string]::IsNullOrEmpty($remoteFileSize)) -and !([string]::IsNullOrEmpty($fileSize)) ) { + if ($remoteFileSize -ne $fileSize) { + Say "The remote and local file sizes are not equal. Remote file size is $remoteFileSize bytes and local size is $fileSize bytes. The local package may be corrupted." + } + else { + Say "The remote and local file sizes are equal." + } + } + else { + Say "Either downloaded or local package size can not be measured. One of them may be corrupted." + } + } + catch { + Say "Either downloaded or local package size can not be measured. One of them may be corrupted." + } +} + +function SafeRemoveFile($Path) { + try { + if (Test-Path $Path) { + Remove-Item $Path + Say-Verbose "The temporary file `"$Path`" was removed." + } + else { + Say-Verbose "The temporary file `"$Path`" does not exist, therefore is not removed." + } + } + catch { + Say-Warning "Failed to remove the temporary file: `"$Path`", remove it manually." + } +} + +function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot) { + $BinPath = Get-Absolute-Path $(Join-Path -Path $InstallRoot -ChildPath "") + if (-Not $NoPath) { + $SuffixedBinPath = "$BinPath;" + if (-Not $env:path.Contains($SuffixedBinPath)) { + Say "Adding to current process PATH: `"$BinPath`". Note: This change will not be visible if PowerShell was run as a child process." + $env:path = $SuffixedBinPath + $env:path + } + else { + Say-Verbose "Current process PATH already contains `"$BinPath`"" + } + } + else { + Say "Binaries of dotnet can be found in $BinPath" + } +} + +function PrintDryRunOutput($Invocation, $DownloadLinks) { + Say "Payload URLs:" + + for ($linkIndex = 0; $linkIndex -lt $DownloadLinks.count; $linkIndex++) { + Say "URL #$linkIndex - $($DownloadLinks[$linkIndex].type): $($DownloadLinks[$linkIndex].downloadLink)" + } + $RepeatableCommand = ".\$ScriptName -Version `"$SpecificVersion`" -InstallDir `"$InstallRoot`" -Architecture `"$CLIArchitecture`"" + if ($Runtime -eq "dotnet") { + $RepeatableCommand += " -Runtime `"dotnet`"" + } + elseif ($Runtime -eq "aspnetcore") { + $RepeatableCommand += " -Runtime `"aspnetcore`"" + } + + foreach ($key in $Invocation.BoundParameters.Keys) { + if (-not (@("Architecture", "Channel", "DryRun", "InstallDir", "Runtime", "SharedRuntime", "Version", "Quality", "FeedCredential") -contains $key)) { + $RepeatableCommand += " -$key `"$($Invocation.BoundParameters[$key])`"" + } + } + if ($Invocation.BoundParameters.Keys -contains "FeedCredential") { + $RepeatableCommand += " -FeedCredential `"`"" + } + Say "Repeatable invocation: $RepeatableCommand" + if ($SpecificVersion -ne $EffectiveVersion) { + Say "NOTE: Due to finding a version manifest with this runtime, it would actually install with version '$EffectiveVersion'" + } +} + +function Get-AkaMSDownloadLink([string]$Channel, [string]$Quality, [bool]$Internal, [string]$Product, [string]$Architecture) { + Say-Invocation $MyInvocation + + #quality is not supported for LTS or STS channel + if (![string]::IsNullOrEmpty($Quality) -and (@("LTS", "STS") -contains $Channel)) { + $Quality = "" + Say-Warning "Specifying quality for STS or LTS channel is not supported, the quality will be ignored." + } + Say-Verbose "Retrieving primary payload URL from aka.ms link for channel: '$Channel', quality: '$Quality' product: '$Product', os: 'win', architecture: '$Architecture'." + + #construct aka.ms link + $akaMsLink = "https://aka.ms/dotnet" + if ($Internal) { + $akaMsLink += "/internal" + } + $akaMsLink += "/$Channel" + if (-not [string]::IsNullOrEmpty($Quality)) { + $akaMsLink += "/$Quality" + } + $akaMsLink += "/$Product-win-$Architecture.zip" + Say-Verbose "Constructed aka.ms link: '$akaMsLink'." + $akaMsDownloadLink = $null + + for ($maxRedirections = 9; $maxRedirections -ge 0; $maxRedirections--) { + #get HTTP response + #do not pass credentials as a part of the $akaMsLink and do not apply credentials in the GetHTTPResponse function + #otherwise the redirect link would have credentials as well + #it would result in applying credentials twice to the resulting link and thus breaking it, and in echoing credentials to the output as a part of redirect link + $Response = GetHTTPResponse -Uri $akaMsLink -HeaderOnly $true -DisableRedirect $true -DisableFeedCredential $true + Say-Verbose "Received response:`n$Response" + + if ([string]::IsNullOrEmpty($Response)) { + Say-Verbose "The link '$akaMsLink' is not valid: failed to get redirect location. The resource is not available." + return $null + } + + #if HTTP code is 301 (Moved Permanently), the redirect link exists + if ($Response.StatusCode -eq 301) { + try { + $akaMsDownloadLink = $Response.Headers.GetValues("Location")[0] + + if ([string]::IsNullOrEmpty($akaMsDownloadLink)) { + Say-Verbose "The link '$akaMsLink' is not valid: server returned 301 (Moved Permanently), but the headers do not contain the redirect location." + return $null + } + + Say-Verbose "The redirect location retrieved: '$akaMsDownloadLink'." + # This may yet be a link to another redirection. Attempt to retrieve the page again. + $akaMsLink = $akaMsDownloadLink + continue + } + catch { + Say-Verbose "The link '$akaMsLink' is not valid: failed to get redirect location." + return $null + } + } + elseif ((($Response.StatusCode -lt 300) -or ($Response.StatusCode -ge 400)) -and (-not [string]::IsNullOrEmpty($akaMsDownloadLink))) { + # Redirections have ended. + return $akaMsDownloadLink + } + + Say-Verbose "The link '$akaMsLink' is not valid: failed to retrieve the redirection location." + return $null + } + + Say-Verbose "Aka.ms links have redirected more than the maximum allowed redirections. This may be caused by a cyclic redirection of aka.ms links." + return $null + +} + +function Get-AkaMsLink-And-Version([string] $NormalizedChannel, [string] $NormalizedQuality, [bool] $Internal, [string] $ProductName, [string] $Architecture) { + $AkaMsDownloadLink = Get-AkaMSDownloadLink -Channel $NormalizedChannel -Quality $NormalizedQuality -Internal $Internal -Product $ProductName -Architecture $Architecture + + if ([string]::IsNullOrEmpty($AkaMsDownloadLink)) { + if (-not [string]::IsNullOrEmpty($NormalizedQuality)) { + # if quality is specified - exit with error - there is no fallback approach + Say-Error "Failed to locate the latest version in the channel '$NormalizedChannel' with '$NormalizedQuality' quality for '$ProductName', os: 'win', architecture: '$Architecture'." + Say-Error "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support." + throw "aka.ms link resolution failure" + } + Say-Verbose "Falling back to latest.version file approach." + return ($null, $null, $null) + } + else { + Say-Verbose "Retrieved primary named payload URL from aka.ms link: '$AkaMsDownloadLink'." + Say-Verbose "Downloading using legacy url will not be attempted." + + #get version from the path + $pathParts = $AkaMsDownloadLink.Split('/') + if ($pathParts.Length -ge 2) { + $SpecificVersion = $pathParts[$pathParts.Length - 2] + Say-Verbose "Version: '$SpecificVersion'." + } + else { + Say-Error "Failed to extract the version from download link '$AkaMsDownloadLink'." + return ($null, $null, $null) + } + + #retrieve effective (product) version + $EffectiveVersion = Get-Product-Version -SpecificVersion $SpecificVersion -PackageDownloadLink $AkaMsDownloadLink + Say-Verbose "Product version: '$EffectiveVersion'." + + return ($AkaMsDownloadLink, $SpecificVersion, $EffectiveVersion); + } +} + +function Get-Feeds-To-Use() { + $feeds = @( + "https://builds.dotnet.microsoft.com/dotnet" + "https://ci.dot.net/public" + ) + + if (-not [string]::IsNullOrEmpty($AzureFeed)) { + $feeds = @($AzureFeed) + } + + if (-not [string]::IsNullOrEmpty($UncachedFeed)) { + $feeds = @($UncachedFeed) + } + + Write-Verbose "Initialized feeds: $feeds" + + return $feeds +} + +function Resolve-AssetName-And-RelativePath([string] $Runtime) { + + if ($Runtime -eq "dotnet") { + $assetName = ".NET Core Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.NETCore.App" + } + elseif ($Runtime -eq "aspnetcore") { + $assetName = "ASP.NET Core Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.AspNetCore.App" + } + elseif ($Runtime -eq "windowsdesktop") { + $assetName = ".NET Core Windows Desktop Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.WindowsDesktop.App" + } + elseif (-not $Runtime) { + $assetName = ".NET Core SDK" + $dotnetPackageRelativePath = "sdk" + } + else { + throw "Invalid value for `$Runtime" + } + + return ($assetName, $dotnetPackageRelativePath) +} + +function Prepare-Install-Directory { + $diskSpaceWarning = "Failed to check the disk space. Installation will continue, but it may fail if you do not have enough disk space."; + + if ($PSVersionTable.PSVersion.Major -lt 7) { + Say-Verbose $diskSpaceWarning + return + } + + New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null + + $installDrive = $((Get-Item $InstallRoot -Force).PSDrive.Name); + $diskInfo = $null + try { + $diskInfo = Get-PSDrive -Name $installDrive + } + catch { + Say-Warning $diskSpaceWarning + } + + # The check is relevant for PS version >= 7, the result can be irrelevant for older versions. See https://github.com/PowerShell/PowerShell/issues/12442. + if ( ($null -ne $diskInfo) -and ($diskInfo.Free / 1MB -le 100)) { + throw "There is not enough disk space on drive ${installDrive}:" + } +} + +if ($Help) { + Get-Help $PSCommandPath -Examples + exit +} + +Say-Verbose "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" +Say-Verbose "- The SDK needs to be installed without user interaction and without admin rights." +Say-Verbose "- The SDK installation doesn't need to persist across multiple CI runs." +Say-Verbose "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.`r`n" + +if ($SharedRuntime -and (-not $Runtime)) { + $Runtime = "dotnet" +} + +$OverrideNonVersionedFiles = !$SkipNonVersionedFiles + +Measure-Action "Product discovery" { + $script:CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture + $script:NormalizedQuality = Get-NormalizedQuality $Quality + Say-Verbose "Normalized quality: '$NormalizedQuality'" + $script:NormalizedChannel = Get-NormalizedChannel $Channel + Say-Verbose "Normalized channel: '$NormalizedChannel'" + $script:NormalizedProduct = Get-NormalizedProduct $Runtime + Say-Verbose "Normalized product: '$NormalizedProduct'" + $script:FeedCredential = ValidateFeedCredential $FeedCredential +} + +$InstallRoot = Resolve-Installation-Path $InstallDir +if (-not (Test-User-Write-Access $InstallRoot)) { + Say-Error "The current user doesn't have write access to the installation root '$InstallRoot' to install .NET. Please try specifying a different installation directory using the -InstallDir parameter, or ensure the selected directory has the appropriate permissions." + throw +} +Say-Verbose "InstallRoot: $InstallRoot" +$ScriptName = $MyInvocation.MyCommand.Name +($assetName, $dotnetPackageRelativePath) = Resolve-AssetName-And-RelativePath -Runtime $Runtime + +$feeds = Get-Feeds-To-Use +$DownloadLinks = @() + +if ($Version.ToLowerInvariant() -ne "latest" -and -not [string]::IsNullOrEmpty($Quality)) { + throw "Quality and Version options are not allowed to be specified simultaneously. See https:// learn.microsoft.com/dotnet/core/tools/dotnet-install-script#options for details." +} + +# aka.ms links can only be used if the user did not request a specific version via the command line or a global.json file. +if ([string]::IsNullOrEmpty($JSonFile) -and ($Version -eq "latest")) { + ($DownloadLink, $SpecificVersion, $EffectiveVersion) = Get-AkaMsLink-And-Version $NormalizedChannel $NormalizedQuality $Internal $NormalizedProduct $CLIArchitecture + + if ($null -ne $DownloadLink) { + $DownloadLinks += New-Object PSObject -Property @{downloadLink = "$DownloadLink"; specificVersion = "$SpecificVersion"; effectiveVersion = "$EffectiveVersion"; type = 'aka.ms' } + Say-Verbose "Generated aka.ms link $DownloadLink with version $EffectiveVersion" + + if (-Not $DryRun) { + Say-Verbose "Checking if the version $EffectiveVersion is already installed" + if (Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $EffectiveVersion) { + Say "$assetName with version '$EffectiveVersion' is already installed." + Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot + return + } + } + } +} + +# Primary and legacy links cannot be used if a quality was specified. +# If we already have an aka.ms link, no need to search the blob feeds. +if ([string]::IsNullOrEmpty($NormalizedQuality) -and 0 -eq $DownloadLinks.count) { + foreach ($feed in $feeds) { + try { + $SpecificVersion = Get-Specific-Version-From-Version -AzureFeed $feed -Channel $Channel -Version $Version -JSonFile $JSonFile + $DownloadLink, $EffectiveVersion = Get-Download-Link -AzureFeed $feed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture + $LegacyDownloadLink = Get-LegacyDownload-Link -AzureFeed $feed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture + + $DownloadLinks += New-Object PSObject -Property @{downloadLink = "$DownloadLink"; specificVersion = "$SpecificVersion"; effectiveVersion = "$EffectiveVersion"; type = 'primary' } + Say-Verbose "Generated primary link $DownloadLink with version $EffectiveVersion" + + if (-not [string]::IsNullOrEmpty($LegacyDownloadLink)) { + $DownloadLinks += New-Object PSObject -Property @{downloadLink = "$LegacyDownloadLink"; specificVersion = "$SpecificVersion"; effectiveVersion = "$EffectiveVersion"; type = 'legacy' } + Say-Verbose "Generated legacy link $LegacyDownloadLink with version $EffectiveVersion" + } + + if (-Not $DryRun) { + Say-Verbose "Checking if the version $EffectiveVersion is already installed" + if (Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $EffectiveVersion) { + Say "$assetName with version '$EffectiveVersion' is already installed." + Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot + return + } + } + } + catch { + Say-Verbose "Failed to acquire download links from feed $feed. Exception: $_" + } + } +} + +if ($DownloadLinks.count -eq 0) { + throw "Failed to resolve the exact version number." +} + +if ($DryRun) { + PrintDryRunOutput $MyInvocation $DownloadLinks + return +} + +Measure-Action "Installation directory preparation" { Prepare-Install-Directory } + +Say-Verbose "Zip path: $ZipPath" + +$DownloadSucceeded = $false +$DownloadedLink = $null +$ErrorMessages = @() + +foreach ($link in $DownloadLinks) { + Say-Verbose "Downloading `"$($link.type)`" link $($link.downloadLink)" + + try { + Measure-Action "Package download" { DownloadFile -Source $link.downloadLink -OutPath $ZipPath } + Say-Verbose "Download succeeded." + $DownloadSucceeded = $true + $DownloadedLink = $link + break + } + catch { + $StatusCode = $null + $ErrorMessage = $null + + if ($PSItem.Exception.Data.Contains("StatusCode")) { + $StatusCode = $PSItem.Exception.Data["StatusCode"] + } + + if ($PSItem.Exception.Data.Contains("ErrorMessage")) { + $ErrorMessage = $PSItem.Exception.Data["ErrorMessage"] + } + else { + $ErrorMessage = $PSItem.Exception.Message + } + + Say-Verbose "Download failed with status code $StatusCode. Error message: $ErrorMessage" + $ErrorMessages += "Downloading from `"$($link.type)`" link has failed with error:`nUri: $($link.downloadLink)`nStatusCode: $StatusCode`nError: $ErrorMessage" + } + + # This link failed. Clean up before trying the next one. + SafeRemoveFile -Path $ZipPath +} + +if (-not $DownloadSucceeded) { + foreach ($ErrorMessage in $ErrorMessages) { + Say-Error $ErrorMessages + } + + throw "Could not find `"$assetName`" with version = $($DownloadLinks[0].effectiveVersion)`nRefer to: https://aka.ms/dotnet-os-lifecycle for information on .NET support" +} + +Say "Extracting the archive." +Measure-Action "Package extraction" { Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot } + +# Check if the SDK version is installed; if not, fail the installation. +$isAssetInstalled = $false + +# if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed. +if ($DownloadedLink.effectiveVersion -Match "rtm" -or $DownloadedLink.effectiveVersion -Match "servicing") { + $ReleaseVersion = $DownloadedLink.effectiveVersion.Split("-")[0] + Say-Verbose "Checking installation: version = $ReleaseVersion" + $isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $ReleaseVersion +} + +# Check if the SDK version is installed. +if (!$isAssetInstalled) { + Say-Verbose "Checking installation: version = $($DownloadedLink.effectiveVersion)" + $isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $DownloadedLink.effectiveVersion +} + +# Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm. +if (!$isAssetInstalled) { + Say-Error "Failed to verify the version of installed `"$assetName`".`nInstallation source: $($DownloadedLink.downloadLink).`nInstallation location: $InstallRoot.`nReport the bug at https://github.com/dotnet/install-scripts/issues." + throw "`"$assetName`" with version = $($DownloadedLink.effectiveVersion) failed to install with an unknown error." +} + +if (-not $KeepZip) { + SafeRemoveFile -Path $ZipPath +} + +Measure-Action "Setting up shell environment" { Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot } + +Say "Note that the script does not ensure your Windows version is supported during the installation." +Say "To check the list of supported versions, go to https://learn.microsoft.com/dotnet/core/install/windows#supported-versions" +Say "Installed version is $($DownloadedLink.effectiveVersion)" +Say "Installation finished" diff --git a/tools/dotnet-install.sh b/tools/dotnet-install.sh new file mode 100644 index 0000000..6180745 --- /dev/null +++ b/tools/dotnet-install.sh @@ -0,0 +1,1888 @@ +#!/usr/bin/env bash +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +# Stop script on NZEC +set -e +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u +# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success +# This is causing it to fail +set -o pipefail + +# Use in the the functions: eval $invocation +invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' + +# standard output may be used as a return value in the functions +# we need a way to write text on the screen in the functions so that +# it won't interfere with the return value. +# Exposing stream 3 as a pipe to standard output of the script itself +exec 3>&1 + +# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors. +# See if stdout is a terminal +if [ -t 1 ] && command -v tput > /dev/null; then + # see if it supports colors + ncolors=$(tput colors || echo 0) + if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then + bold="$(tput bold || echo)" + normal="$(tput sgr0 || echo)" + black="$(tput setaf 0 || echo)" + red="$(tput setaf 1 || echo)" + green="$(tput setaf 2 || echo)" + yellow="$(tput setaf 3 || echo)" + blue="$(tput setaf 4 || echo)" + magenta="$(tput setaf 5 || echo)" + cyan="$(tput setaf 6 || echo)" + white="$(tput setaf 7 || echo)" + fi +fi + +say_warning() { + printf "%b\n" "${yellow:-}dotnet_install: Warning: $1${normal:-}" >&3 +} + +say_err() { + printf "%b\n" "${red:-}dotnet_install: Error: $1${normal:-}" >&2 +} + +say() { + # using stream 3 (defined in the beginning) to not interfere with stdout of functions + # which may be used as return value + printf "%b\n" "${cyan:-}dotnet-install:${normal:-} $1" >&3 +} + +say_verbose() { + if [ "$verbose" = true ]; then + say "$1" + fi +} + +# This platform list is finite - if the SDK/Runtime has supported Linux distribution-specific assets, +# then and only then should the Linux distribution appear in this list. +# Adding a Linux distribution to this list does not imply distribution-specific support. +get_legacy_os_name_from_platform() { + eval $invocation + + platform="$1" + case "$platform" in + "centos.7") + echo "centos" + return 0 + ;; + "debian.8") + echo "debian" + return 0 + ;; + "debian.9") + echo "debian.9" + return 0 + ;; + "fedora.23") + echo "fedora.23" + return 0 + ;; + "fedora.24") + echo "fedora.24" + return 0 + ;; + "fedora.27") + echo "fedora.27" + return 0 + ;; + "fedora.28") + echo "fedora.28" + return 0 + ;; + "opensuse.13.2") + echo "opensuse.13.2" + return 0 + ;; + "opensuse.42.1") + echo "opensuse.42.1" + return 0 + ;; + "opensuse.42.3") + echo "opensuse.42.3" + return 0 + ;; + "rhel.7"*) + echo "rhel" + return 0 + ;; + "ubuntu.14.04") + echo "ubuntu" + return 0 + ;; + "ubuntu.16.04") + echo "ubuntu.16.04" + return 0 + ;; + "ubuntu.16.10") + echo "ubuntu.16.10" + return 0 + ;; + "ubuntu.18.04") + echo "ubuntu.18.04" + return 0 + ;; + "alpine.3.4.3") + echo "alpine" + return 0 + ;; + esac + return 1 +} + +get_legacy_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + echo "osx" + return 0 + elif [ -n "$runtime_id" ]; then + echo $(get_legacy_os_name_from_platform "${runtime_id%-*}" || echo "${runtime_id%-*}") + return 0 + else + if [ -e /etc/os-release ]; then + . /etc/os-release + os=$(get_legacy_os_name_from_platform "$ID${VERSION_ID:+.${VERSION_ID}}" || echo "") + if [ -n "$os" ]; then + echo "$os" + return 0 + fi + fi + fi + + say_verbose "Distribution specific OS name and version could not be detected: UName = $uname" + return 1 +} + +get_linux_platform_name() { + eval $invocation + + if [ -n "$runtime_id" ]; then + echo "${runtime_id%-*}" + return 0 + else + if [ -e /etc/os-release ]; then + . /etc/os-release + echo "$ID${VERSION_ID:+.${VERSION_ID}}" + return 0 + elif [ -e /etc/redhat-release ]; then + local redhatRelease=$(&1 || true) | grep -q musl +} + +get_current_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + echo "osx" + return 0 + elif [ "$uname" = "FreeBSD" ]; then + echo "freebsd" + return 0 + elif [ "$uname" = "Linux" ]; then + local linux_platform_name="" + linux_platform_name="$(get_linux_platform_name)" || true + + if [ "$linux_platform_name" = "rhel.6" ]; then + echo $linux_platform_name + return 0 + elif is_musl_based_distro; then + echo "linux-musl" + return 0 + elif [ "$linux_platform_name" = "linux-musl" ]; then + echo "linux-musl" + return 0 + else + echo "linux" + return 0 + fi + fi + + say_err "OS name could not be detected: UName = $uname" + return 1 +} + +machine_has() { + eval $invocation + + command -v "$1" > /dev/null 2>&1 + return $? +} + +check_min_reqs() { + local hasMinimum=false + if machine_has "curl"; then + hasMinimum=true + elif machine_has "wget"; then + hasMinimum=true + fi + + if [ "$hasMinimum" = "false" ]; then + say_err "curl (recommended) or wget are required to download dotnet. Install missing prerequisite to proceed." + return 1 + fi + return 0 +} + +# args: +# input - $1 +to_lowercase() { + #eval $invocation + + echo "$1" | tr '[:upper:]' '[:lower:]' + return 0 +} + +# args: +# input - $1 +remove_trailing_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input%/}" + return 0 +} + +# args: +# input - $1 +remove_beginning_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input#/}" + return 0 +} + +# args: +# root_path - $1 +# child_path - $2 - this parameter can be empty +combine_paths() { + eval $invocation + + # TODO: Consider making it work with any number of paths. For now: + if [ ! -z "${3:-}" ]; then + say_err "combine_paths: Function takes two parameters." + return 1 + fi + + local root_path="$(remove_trailing_slash "$1")" + local child_path="$(remove_beginning_slash "${2:-}")" + say_verbose "combine_paths: root_path=$root_path" + say_verbose "combine_paths: child_path=$child_path" + echo "$root_path/$child_path" + return 0 +} + +get_machine_architecture() { + eval $invocation + + if command -v uname > /dev/null; then + CPUName=$(uname -m) + case $CPUName in + armv1*|armv2*|armv3*|armv4*|armv5*|armv6*) + echo "armv6-or-below" + return 0 + ;; + armv*l) + echo "arm" + return 0 + ;; + aarch64|arm64) + if [ "$(getconf LONG_BIT)" -lt 64 ]; then + # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS) + echo "arm" + return 0 + fi + echo "arm64" + return 0 + ;; + s390x) + echo "s390x" + return 0 + ;; + ppc64le) + echo "ppc64le" + return 0 + ;; + loongarch64) + echo "loongarch64" + return 0 + ;; + riscv64) + echo "riscv64" + return 0 + ;; + powerpc|ppc) + echo "ppc" + return 0 + ;; + esac + fi + + # Always default to 'x64' + echo "x64" + return 0 +} + +# args: +# architecture - $1 +get_normalized_architecture_from_architecture() { + eval $invocation + + local architecture="$(to_lowercase "$1")" + + if [[ $architecture == \ ]]; then + machine_architecture="$(get_machine_architecture)" + if [[ "$machine_architecture" == "armv6-or-below" ]]; then + say_err "Architecture \`$machine_architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" + return 1 + fi + + echo $machine_architecture + return 0 + fi + + case "$architecture" in + amd64|x64) + echo "x64" + return 0 + ;; + arm) + echo "arm" + return 0 + ;; + arm64) + echo "arm64" + return 0 + ;; + s390x) + echo "s390x" + return 0 + ;; + ppc64le) + echo "ppc64le" + return 0 + ;; + loongarch64) + echo "loongarch64" + return 0 + ;; + esac + + say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" + return 1 +} + +# args: +# version - $1 +# channel - $2 +# architecture - $3 +get_normalized_architecture_for_specific_sdk_version() { + eval $invocation + + local is_version_support_arm64="$(is_arm64_supported "$1")" + local is_channel_support_arm64="$(is_arm64_supported "$2")" + local architecture="$3"; + local osname="$(get_current_os_name)" + + if [ "$osname" == "osx" ] && [ "$architecture" == "arm64" ] && { [ "$is_version_support_arm64" = false ] || [ "$is_channel_support_arm64" = false ]; }; then + #check if rosetta is installed + if [ "$(/usr/bin/pgrep oahd >/dev/null 2>&1;echo $?)" -eq 0 ]; then + say_verbose "Changing user architecture from '$architecture' to 'x64' because .NET SDKs prior to version 6.0 do not support arm64." + echo "x64" + return 0; + else + say_err "Architecture \`$architecture\` is not supported for .NET SDK version \`$version\`. Please install Rosetta to allow emulation of the \`$architecture\` .NET SDK on this platform" + return 1 + fi + fi + + echo "$architecture" + return 0 +} + +# args: +# version or channel - $1 +is_arm64_supported() { + # Extract the major version by splitting on the dot + major_version="${1%%.*}" + + # Check if the major version is a valid number and less than 6 + case "$major_version" in + [0-9]*) + if [ "$major_version" -lt 6 ]; then + echo false + return 0 + fi + ;; + esac + + echo true + return 0 +} + +# args: +# user_defined_os - $1 +get_normalized_os() { + eval $invocation + + local osname="$(to_lowercase "$1")" + if [ ! -z "$osname" ]; then + case "$osname" in + osx | freebsd | rhel.6 | linux-musl | linux) + echo "$osname" + return 0 + ;; + macos) + osname='osx' + echo "$osname" + return 0 + ;; + *) + say_err "'$user_defined_os' is not a supported value for --os option, supported values are: osx, macos, linux, linux-musl, freebsd, rhel.6. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." + return 1 + ;; + esac + else + osname="$(get_current_os_name)" || return 1 + fi + echo "$osname" + return 0 +} + +# args: +# quality - $1 +get_normalized_quality() { + eval $invocation + + local quality="$(to_lowercase "$1")" + if [ ! -z "$quality" ]; then + case "$quality" in + daily | preview) + echo "$quality" + return 0 + ;; + ga) + #ga quality is available without specifying quality, so normalizing it to empty + return 0 + ;; + *) + say_err "'$quality' is not a supported value for --quality option. Supported values are: daily, preview, ga. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." + return 1 + ;; + esac + fi + return 0 +} + +# args: +# channel - $1 +get_normalized_channel() { + eval $invocation + + local channel="$(to_lowercase "$1")" + + if [[ $channel == current ]]; then + say_warning 'Value "Current" is deprecated for -Channel option. Use "STS" instead.' + fi + + if [[ $channel == release/* ]]; then + say_warning 'Using branch name with -Channel option is no longer supported with newer releases. Use -Quality option with a channel in X.Y format instead.'; + fi + + if [ ! -z "$channel" ]; then + case "$channel" in + lts) + echo "LTS" + return 0 + ;; + sts) + echo "STS" + return 0 + ;; + current) + echo "STS" + return 0 + ;; + *) + echo "$channel" + return 0 + ;; + esac + fi + + return 0 +} + +# args: +# runtime - $1 +get_normalized_product() { + eval $invocation + + local product="" + local runtime="$(to_lowercase "$1")" + if [[ "$runtime" == "dotnet" ]]; then + product="dotnet-runtime" + elif [[ "$runtime" == "aspnetcore" ]]; then + product="aspnetcore-runtime" + elif [ -z "$runtime" ]; then + product="dotnet-sdk" + fi + echo "$product" + return 0 +} + +# The version text returned from the feeds is a 1-line or 2-line string: +# For the SDK and the dotnet runtime (2 lines): +# Line 1: # commit_hash +# Line 2: # 4-part version +# For the aspnetcore runtime (1 line): +# Line 1: # 4-part version + +# args: +# version_text - stdin +get_version_from_latestversion_file_content() { + eval $invocation + + cat | tail -n 1 | sed 's/\r$//' + return 0 +} + +# args: +# install_root - $1 +# relative_path_to_package - $2 +# specific_version - $3 +is_dotnet_package_installed() { + eval $invocation + + local install_root="$1" + local relative_path_to_package="$2" + local specific_version="${3//[$'\t\r\n']}" + + local dotnet_package_path="$(combine_paths "$(combine_paths "$install_root" "$relative_path_to_package")" "$specific_version")" + say_verbose "is_dotnet_package_installed: dotnet_package_path=$dotnet_package_path" + + if [ -d "$dotnet_package_path" ]; then + return 0 + else + return 1 + fi +} + +# args: +# downloaded file - $1 +# remote_file_size - $2 +validate_remote_local_file_sizes() +{ + eval $invocation + + local downloaded_file="$1" + local remote_file_size="$2" + local file_size='' + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + file_size="$(stat -c '%s' "$downloaded_file")" + elif [[ "$OSTYPE" == "darwin"* ]]; then + # hardcode in order to avoid conflicts with GNU stat + file_size="$(/usr/bin/stat -f '%z' "$downloaded_file")" + fi + + if [ -n "$file_size" ]; then + say "Downloaded file size is $file_size bytes." + + if [ -n "$remote_file_size" ] && [ -n "$file_size" ]; then + if [ "$remote_file_size" -ne "$file_size" ]; then + say "The remote and local file sizes are not equal. The remote file size is $remote_file_size bytes and the local size is $file_size bytes. The local package may be corrupted." + else + say "The remote and local file sizes are equal." + fi + fi + + else + say "Either downloaded or local package size can not be measured. One of them may be corrupted." + fi +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +get_version_from_latestversion_file() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + + local version_file_url=null + if [[ "$runtime" == "dotnet" ]]; then + version_file_url="$azure_feed/Runtime/$channel/latest.version" + elif [[ "$runtime" == "aspnetcore" ]]; then + version_file_url="$azure_feed/aspnetcore/Runtime/$channel/latest.version" + elif [ -z "$runtime" ]; then + version_file_url="$azure_feed/Sdk/$channel/latest.version" + else + say_err "Invalid value for \$runtime" + return 1 + fi + say_verbose "get_version_from_latestversion_file: latest url: $version_file_url" + + download "$version_file_url" || return $? + return 0 +} + +# args: +# json_file - $1 +parse_globaljson_file_for_version() { + eval $invocation + + local json_file="$1" + if [ ! -f "$json_file" ]; then + say_err "Unable to find \`$json_file\`" + return 1 + fi + + sdk_section=$(cat "$json_file" | tr -d "\r" | awk '/"sdk"/,/}/') + if [ -z "$sdk_section" ]; then + say_err "Unable to parse the SDK node in \`$json_file\`" + return 1 + fi + + sdk_list=$(echo $sdk_section | awk -F"[{}]" '{print $2}') + sdk_list=${sdk_list//[\" ]/} + sdk_list=${sdk_list//,/$'\n'} + + local version_info="" + while read -r line; do + IFS=: + while read -r key value; do + if [[ "$key" == "version" ]]; then + version_info=$value + fi + done <<< "$line" + done <<< "$sdk_list" + if [ -z "$version_info" ]; then + say_err "Unable to find the SDK:version node in \`$json_file\`" + return 1 + fi + + unset IFS; + echo "$version_info" + return 0 +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# version - $4 +# json_file - $5 +get_specific_version_from_version() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local version="$(to_lowercase "$4")" + local json_file="$5" + + if [ -z "$json_file" ]; then + if [[ "$version" == "latest" ]]; then + local version_info + version_info="$(get_version_from_latestversion_file "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1 + say_verbose "get_specific_version_from_version: version_info=$version_info" + echo "$version_info" | get_version_from_latestversion_file_content + return 0 + else + echo "$version" + return 0 + fi + else + local version_info + version_info="$(parse_globaljson_file_for_version "$json_file")" || return 1 + echo "$version_info" + return 0 + fi +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +# normalized_os - $5 +construct_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + local specific_product_version="$(get_specific_product_version "$1" "$4")" + local osname="$5" + + local download_link=null + if [[ "$runtime" == "dotnet" ]]; then + download_link="$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz" + elif [[ "$runtime" == "aspnetcore" ]]; then + download_link="$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz" + elif [ -z "$runtime" ]; then + download_link="$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_product_version-$osname-$normalized_architecture.tar.gz" + else + return 1 + fi + + echo "$download_link" + return 0 +} + +# args: +# azure_feed - $1 +# specific_version - $2 +# download link - $3 (optional) +get_specific_product_version() { + # If we find a 'productVersion.txt' at the root of any folder, we'll use its contents + # to resolve the version of what's in the folder, superseding the specified version. + # if 'productVersion.txt' is missing but download link is already available, product version will be taken from download link + eval $invocation + + local azure_feed="$1" + local specific_version="${2//[$'\t\r\n']}" + local package_download_link="" + if [ $# -gt 2 ]; then + local package_download_link="$3" + fi + local specific_product_version=null + + # Try to get the version number, using the productVersion.txt file located next to the installer file. + local download_links=($(get_specific_product_version_url "$azure_feed" "$specific_version" true "$package_download_link") + $(get_specific_product_version_url "$azure_feed" "$specific_version" false "$package_download_link")) + + for download_link in "${download_links[@]}" + do + say_verbose "Checking for the existence of $download_link" + + if machine_has "curl" + then + if ! specific_product_version=$(curl -s --fail "${download_link}${feed_credential}" 2>&1); then + continue + else + echo "${specific_product_version//[$'\t\r\n']}" + return 0 + fi + + elif machine_has "wget" + then + specific_product_version=$(wget -qO- "${download_link}${feed_credential}" 2>&1) + if [ $? = 0 ]; then + echo "${specific_product_version//[$'\t\r\n']}" + return 0 + fi + fi + done + + # Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number. + say_verbose "Failed to get the version using productVersion.txt file. Download link will be parsed instead." + specific_product_version="$(get_product_specific_version_from_download_link "$package_download_link" "$specific_version")" + echo "${specific_product_version//[$'\t\r\n']}" + return 0 +} + +# args: +# azure_feed - $1 +# specific_version - $2 +# is_flattened - $3 +# download link - $4 (optional) +get_specific_product_version_url() { + eval $invocation + + local azure_feed="$1" + local specific_version="$2" + local is_flattened="$3" + local package_download_link="" + if [ $# -gt 3 ]; then + local package_download_link="$4" + fi + + local pvFileName="productVersion.txt" + if [ "$is_flattened" = true ]; then + if [ -z "$runtime" ]; then + pvFileName="sdk-productVersion.txt" + elif [[ "$runtime" == "dotnet" ]]; then + pvFileName="runtime-productVersion.txt" + else + pvFileName="$runtime-productVersion.txt" + fi + fi + + local download_link=null + + if [ -z "$package_download_link" ]; then + if [[ "$runtime" == "dotnet" ]]; then + download_link="$azure_feed/Runtime/$specific_version/${pvFileName}" + elif [[ "$runtime" == "aspnetcore" ]]; then + download_link="$azure_feed/aspnetcore/Runtime/$specific_version/${pvFileName}" + elif [ -z "$runtime" ]; then + download_link="$azure_feed/Sdk/$specific_version/${pvFileName}" + else + return 1 + fi + else + download_link="${package_download_link%/*}/${pvFileName}" + fi + + say_verbose "Constructed productVersion link: $download_link" + echo "$download_link" + return 0 +} + +# args: +# download link - $1 +# specific version - $2 +get_product_specific_version_from_download_link() +{ + eval $invocation + + local download_link="$1" + local specific_version="$2" + local specific_product_version="" + + if [ -z "$download_link" ]; then + echo "$specific_version" + return 0 + fi + + #get filename + filename="${download_link##*/}" + + #product specific version follows the product name + #for filename 'dotnet-sdk-3.1.404-linux-x64.tar.gz': the product version is 3.1.404 + IFS='-' + read -ra filename_elems <<< "$filename" + count=${#filename_elems[@]} + if [[ "$count" -gt 2 ]]; then + specific_product_version="${filename_elems[2]}" + else + specific_product_version=$specific_version + fi + unset IFS; + echo "$specific_product_version" + return 0 +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +construct_legacy_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + + local distro_specific_osname + distro_specific_osname="$(get_legacy_os_name)" || return 1 + + local legacy_download_link=null + if [[ "$runtime" == "dotnet" ]]; then + legacy_download_link="$azure_feed/Runtime/$specific_version/dotnet-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + elif [ -z "$runtime" ]; then + legacy_download_link="$azure_feed/Sdk/$specific_version/dotnet-dev-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + else + return 1 + fi + + echo "$legacy_download_link" + return 0 +} + +get_user_install_path() { + eval $invocation + + if [ ! -z "${DOTNET_INSTALL_DIR:-}" ]; then + echo "$DOTNET_INSTALL_DIR" + else + echo "$HOME/.dotnet" + fi + return 0 +} + +# args: +# install_dir - $1 +resolve_installation_path() { + eval $invocation + + local install_dir=$1 + if [ "$install_dir" = "" ]; then + local user_install_path="$(get_user_install_path)" + say_verbose "resolve_installation_path: user_install_path=$user_install_path" + echo "$user_install_path" + return 0 + fi + + echo "$install_dir" + return 0 +} + +# args: +# relative_or_absolute_path - $1 +get_absolute_path() { + eval $invocation + + local relative_or_absolute_path=$1 + echo "$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")" + return 0 +} + +# args: +# override - $1 (boolean, true or false) +get_cp_options() { + eval $invocation + + local override="$1" + local override_switch="" + + if [ "$override" = false ]; then + override_switch="-n" + + # create temporary files to check if 'cp -u' is supported + tmp_dir="$(mktemp -d)" + tmp_file="$tmp_dir/testfile" + tmp_file2="$tmp_dir/testfile2" + + touch "$tmp_file" + + # use -u instead of -n if it's available + if cp -u "$tmp_file" "$tmp_file2" 2>/dev/null; then + override_switch="-u" + fi + + # clean up + rm -f "$tmp_file" "$tmp_file2" + rm -rf "$tmp_dir" + fi + + echo "$override_switch" +} + +# args: +# input_files - stdin +# root_path - $1 +# out_path - $2 +# override - $3 +copy_files_or_dirs_from_list() { + eval $invocation + + local root_path="$(remove_trailing_slash "$1")" + local out_path="$(remove_trailing_slash "$2")" + local override="$3" + local override_switch="$(get_cp_options "$override")" + + cat | uniq | while read -r file_path; do + local path="$(remove_beginning_slash "${file_path#$root_path}")" + local target="$out_path/$path" + if [ "$override" = true ] || (! ([ -d "$target" ] || [ -e "$target" ])); then + mkdir -p "$out_path/$(dirname "$path")" + if [ -d "$target" ]; then + rm -rf "$target" + fi + cp -R $override_switch "$root_path/$path" "$target" + fi + done +} + +# args: +# zip_uri - $1 +get_remote_file_size() { + local zip_uri="$1" + + if machine_has "curl"; then + file_size=$(curl -sI "$zip_uri" | grep -i content-length | awk '{ num = $2 + 0; print num }') + elif machine_has "wget"; then + file_size=$(wget --spider --server-response -O /dev/null "$zip_uri" 2>&1 | grep -i 'Content-Length:' | awk '{ num = $2 + 0; print num }') + else + say "Neither curl nor wget is available on this system." + return + fi + + if [ -n "$file_size" ]; then + say "Remote file $zip_uri size is $file_size bytes." + echo "$file_size" + else + say_verbose "Content-Length header was not extracted for $zip_uri." + echo "" + fi +} + +# args: +# zip_path - $1 +# out_path - $2 +# remote_file_size - $3 +extract_dotnet_package() { + eval $invocation + + local zip_path="$1" + local out_path="$2" + local remote_file_size="$3" + + local temp_out_path="$(mktemp -d "$temporary_file_template")" + + local failed=false + tar -xzf "$zip_path" -C "$temp_out_path" > /dev/null || failed=true + + local folders_with_version_regex='^.*/[0-9]+\.[0-9]+[^/]+/' + find "$temp_out_path" -type f | grep -Eo "$folders_with_version_regex" | sort | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" false + find "$temp_out_path" -type f | grep -Ev "$folders_with_version_regex" | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" "$override_non_versioned_files" + + validate_remote_local_file_sizes "$zip_path" "$remote_file_size" + + rm -rf "$temp_out_path" + if [ -z ${keep_zip+x} ]; then + rm -f "$zip_path" && say_verbose "Temporary archive file $zip_path was removed" + fi + + if [ "$failed" = true ]; then + say_err "Extraction failed" + return 1 + fi + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header() +{ + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + + local failed=false + local response + if machine_has "curl"; then + get_http_header_curl $remote_path $disable_feed_credential || failed=true + elif machine_has "wget"; then + get_http_header_wget $remote_path $disable_feed_credential || failed=true + else + failed=true + fi + if [ "$failed" = true ]; then + say_verbose "Failed to get HTTP header: '$remote_path'." + return 1 + fi + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header_curl() { + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + + remote_path_with_credential="$remote_path" + if [ "$disable_feed_credential" = false ]; then + remote_path_with_credential+="$feed_credential" + fi + + curl_options="-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 " + curl $curl_options "$remote_path_with_credential" 2>&1 || return 1 + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header_wget() { + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + local wget_options="-q -S --spider --tries 5 " + + local wget_options_extra='' + + # Test for options that aren't supported on all wget implementations. + if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then + wget_options_extra="--waitretry 2 --connect-timeout 15 " + else + say "wget extra options are unavailable for this environment" + fi + + remote_path_with_credential="$remote_path" + if [ "$disable_feed_credential" = false ]; then + remote_path_with_credential+="$feed_credential" + fi + + wget $wget_options $wget_options_extra "$remote_path_with_credential" 2>&1 + + return $? +} + +# args: +# remote_path - $1 +# [out_path] - $2 - stdout if not provided +download() { + eval $invocation + + local remote_path="$1" + local out_path="${2:-}" + + if [[ "$remote_path" != "http"* ]]; then + cp "$remote_path" "$out_path" + return $? + fi + + local failed=false + local attempts=0 + while [ $attempts -lt 3 ]; do + attempts=$((attempts+1)) + failed=false + if machine_has "curl"; then + downloadcurl "$remote_path" "$out_path" || failed=true + elif machine_has "wget"; then + downloadwget "$remote_path" "$out_path" || failed=true + else + say_err "Missing dependency: neither curl nor wget was found." + exit 1 + fi + + if [ "$failed" = false ] || [ $attempts -ge 3 ] || { [ -n "${http_code-}" ] && [ "${http_code}" = "404" ]; }; then + break + fi + + say "Download attempt #$attempts has failed: ${http_code-} ${download_error_msg-}" + say "Attempt #$((attempts+1)) will start in $((attempts*10)) seconds." + sleep $((attempts*10)) + done + + if [ "$failed" = true ]; then + say_verbose "Download failed: $remote_path" + return 1 + fi + return 0 +} + +# Updates global variables $http_code and $download_error_msg +downloadcurl() { + eval $invocation + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + # Append feed_credential as late as possible before calling curl to avoid logging feed_credential + # Avoid passing URI with credentials to functions: note, most of them echoing parameters of invocation in verbose output. + local remote_path_with_credential="${remote_path}${feed_credential}" + local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs " + local curl_exit_code=0; + if [ -z "$out_path" ]; then + curl_output=$(curl $curl_options "$remote_path_with_credential" 2>&1) + curl_exit_code=$? + echo "$curl_output" + else + curl_output=$(curl $curl_options -o "$out_path" "$remote_path_with_credential" 2>&1) + curl_exit_code=$? + fi + + # Regression in curl causes curl with --retry to return a 0 exit code even when it fails to download a file - https://github.com/curl/curl/issues/17554 + if [ $curl_exit_code -eq 0 ] && echo "$curl_output" | grep -q "^curl: ([0-9]*) "; then + curl_exit_code=$(echo "$curl_output" | sed 's/curl: (\([0-9]*\)).*/\1/') + fi + + if [ $curl_exit_code -gt 0 ]; then + download_error_msg="Unable to download $remote_path." + # Check for curl timeout codes + if [[ $curl_exit_code == 7 || $curl_exit_code == 28 ]]; then + download_error_msg+=" Failed to reach the server: connection timeout." + else + local disable_feed_credential=false + local response=$(get_http_header_curl $remote_path $disable_feed_credential) + http_code=$( echo "$response" | awk '/^HTTP/{print $2}' | tail -1 ) + if [[ ! -z $http_code && $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + fi + fi + say_verbose "$download_error_msg" + return 1 + fi + return 0 +} + + +# Updates global variables $http_code and $download_error_msg +downloadwget() { + eval $invocation + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + # Append feed_credential as late as possible before calling wget to avoid logging feed_credential + local remote_path_with_credential="${remote_path}${feed_credential}" + local wget_options="--tries 20 " + + local wget_options_extra='' + local wget_result='' + + # Test for options that aren't supported on all wget implementations. + if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then + wget_options_extra="--waitretry 2 --connect-timeout 15 " + else + say "wget extra options are unavailable for this environment" + fi + + if [ -z "$out_path" ]; then + wget -q $wget_options $wget_options_extra -O - "$remote_path_with_credential" 2>&1 + wget_result=$? + else + wget $wget_options $wget_options_extra -O "$out_path" "$remote_path_with_credential" 2>&1 + wget_result=$? + fi + + if [[ $wget_result != 0 ]]; then + local disable_feed_credential=false + local response=$(get_http_header_wget $remote_path $disable_feed_credential) + http_code=$( echo "$response" | awk '/^ HTTP/{print $2}' | tail -1 ) + download_error_msg="Unable to download $remote_path." + if [[ ! -z $http_code && $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + # wget exit code 4 stands for network-issue + elif [[ $wget_result == 4 ]]; then + download_error_msg+=" Failed to reach the server: connection timeout." + fi + say_verbose "$download_error_msg" + return 1 + fi + + return 0 +} + +get_download_link_from_aka_ms() { + eval $invocation + + #quality is not supported for LTS or STS channel + #STS maps to current + if [[ ! -z "$normalized_quality" && ("$normalized_channel" == "LTS" || "$normalized_channel" == "STS") ]]; then + normalized_quality="" + say_warning "Specifying quality for STS or LTS channel is not supported, the quality will be ignored." + fi + + say_verbose "Retrieving primary payload URL from aka.ms for channel: '$normalized_channel', quality: '$normalized_quality', product: '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'." + + #construct aka.ms link + aka_ms_link="https://aka.ms/dotnet" + if [ "$internal" = true ]; then + aka_ms_link="$aka_ms_link/internal" + fi + aka_ms_link="$aka_ms_link/$normalized_channel" + if [[ ! -z "$normalized_quality" ]]; then + aka_ms_link="$aka_ms_link/$normalized_quality" + fi + aka_ms_link="$aka_ms_link/$normalized_product-$normalized_os-$normalized_architecture.tar.gz" + say_verbose "Constructed aka.ms link: '$aka_ms_link'." + + #get HTTP response + #do not pass credentials as a part of the $aka_ms_link and do not apply credentials in the get_http_header function + #otherwise the redirect link would have credentials as well + #it would result in applying credentials twice to the resulting link and thus breaking it, and in echoing credentials to the output as a part of redirect link + disable_feed_credential=true + response="$(get_http_header $aka_ms_link $disable_feed_credential)" + + say_verbose "Received response: $response" + # Get results of all the redirects. + http_codes=$( echo "$response" | awk '$1 ~ /^HTTP/ {print $2}' ) + # They all need to be 301, otherwise some links are broken (except for the last, which is not a redirect but 200 or 404). + broken_redirects=$( echo "$http_codes" | sed '$d' | grep -v '301' ) + # The response may end without final code 2xx/4xx/5xx somehow, e.g. network restrictions on www.bing.com causes redirecting to bing.com fails with connection refused. + # In this case it should not exclude the last. + last_http_code=$( echo "$http_codes" | tail -n 1 ) + if ! [[ $last_http_code =~ ^(2|4|5)[0-9][0-9]$ ]]; then + broken_redirects=$( echo "$http_codes" | grep -v '301' ) + fi + + # All HTTP codes are 301 (Moved Permanently), the redirect link exists. + if [[ -z "$broken_redirects" ]]; then + aka_ms_download_link=$( echo "$response" | awk '$1 ~ /^Location/{print $2}' | tail -1 | tr -d '\r') + + if [[ -z "$aka_ms_download_link" ]]; then + say_verbose "The aka.ms link '$aka_ms_link' is not valid: failed to get redirect location." + return 1 + fi + + say_verbose "The redirect location retrieved: '$aka_ms_download_link'." + return 0 + else + say_verbose "The aka.ms link '$aka_ms_link' is not valid: received HTTP code: $(echo "$broken_redirects" | paste -sd "," -)." + return 1 + fi +} + +get_feeds_to_use() +{ + feeds=( + "https://builds.dotnet.microsoft.com/dotnet" + "https://ci.dot.net/public" + ) + + if [[ -n "$azure_feed" ]]; then + feeds=("$azure_feed") + fi + + if [[ -n "$uncached_feed" ]]; then + feeds=("$uncached_feed") + fi +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed). +generate_download_links() { + + download_links=() + specific_versions=() + effective_versions=() + link_types=() + + # If generate_akams_links returns false, no fallback to old links. Just terminate. + # This function may also 'exit' (if the determined version is already installed). + generate_akams_links || return + + # Check other feeds only if we haven't been able to find an aka.ms link. + if [[ "${#download_links[@]}" -lt 1 ]]; then + for feed in ${feeds[@]} + do + # generate_regular_links may also 'exit' (if the determined version is already installed). + generate_regular_links $feed || return + done + fi + + if [[ "${#download_links[@]}" -eq 0 ]]; then + say_err "Failed to resolve the exact version number." + return 1 + fi + + say_verbose "Generated ${#download_links[@]} links." + for link_index in ${!download_links[@]} + do + say_verbose "Link $link_index: ${link_types[$link_index]}, ${effective_versions[$link_index]}, ${download_links[$link_index]}" + done +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed). +generate_akams_links() { + local valid_aka_ms_link=true; + + normalized_version="$(to_lowercase "$version")" + if [[ "$normalized_version" != "latest" ]] && [ -n "$normalized_quality" ]; then + say_err "Quality and Version options are not allowed to be specified simultaneously. See https://learn.microsoft.com/dotnet/core/tools/dotnet-install-script#options for details." + return 1 + fi + + if [[ -n "$json_file" || "$normalized_version" != "latest" ]]; then + # aka.ms links are not needed when exact version is specified via command or json file + return + fi + + get_download_link_from_aka_ms || valid_aka_ms_link=false + + if [[ "$valid_aka_ms_link" == true ]]; then + say_verbose "Retrieved primary payload URL from aka.ms link: '$aka_ms_download_link'." + say_verbose "Downloading using legacy url will not be attempted." + + download_link=$aka_ms_download_link + + #get version from the path + IFS='/' + read -ra pathElems <<< "$download_link" + count=${#pathElems[@]} + specific_version="${pathElems[count-2]}" + unset IFS; + say_verbose "Version: '$specific_version'." + + #Retrieve effective version + effective_version="$(get_specific_product_version "$azure_feed" "$specific_version" "$download_link")" + + # Add link info to arrays + download_links+=($download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("aka.ms") + + # Check if the SDK version is already installed. + if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "$asset_name with version '$effective_version' is already installed." + exit 0 + fi + + return 0 + fi + + # if quality is specified - exit with error - there is no fallback approach + if [ ! -z "$normalized_quality" ]; then + say_err "Failed to locate the latest version in the channel '$normalized_channel' with '$normalized_quality' quality for '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'." + say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support." + return 1 + fi + say_verbose "Falling back to latest.version file approach." +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed) +# args: +# feed - $1 +generate_regular_links() { + local feed="$1" + local valid_legacy_download_link=true + + specific_version=$(get_specific_version_from_version "$feed" "$channel" "$normalized_architecture" "$version" "$json_file") || specific_version='0' + + if [[ "$specific_version" == '0' ]]; then + say_verbose "Failed to resolve the specific version number using feed '$feed'" + return + fi + + effective_version="$(get_specific_product_version "$feed" "$specific_version")" + say_verbose "specific_version=$specific_version" + + download_link="$(construct_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version" "$normalized_os")" + say_verbose "Constructed primary named payload URL: $download_link" + + # Add link info to arrays + download_links+=($download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("primary") + + legacy_download_link="$(construct_legacy_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version")" || valid_legacy_download_link=false + + if [ "$valid_legacy_download_link" = true ]; then + say_verbose "Constructed legacy named payload URL: $legacy_download_link" + + download_links+=($legacy_download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("legacy") + else + legacy_download_link="" + say_verbose "Could not construct a legacy_download_link; omitting..." + fi + + # Check if the SDK version is already installed. + if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "$asset_name with version '$effective_version' is already installed." + exit 0 + fi +} + +print_dry_run() { + + say "Payload URLs:" + + for link_index in "${!download_links[@]}" + do + say "URL #$link_index - ${link_types[$link_index]}: ${download_links[$link_index]}" + done + + resolved_version=${specific_versions[0]} + repeatable_command="./$script_name --version "\""$resolved_version"\"" --install-dir "\""$install_root"\"" --architecture "\""$normalized_architecture"\"" --os "\""$normalized_os"\""" + + if [ ! -z "$normalized_quality" ]; then + repeatable_command+=" --quality "\""$normalized_quality"\""" + fi + + if [[ "$runtime" == "dotnet" ]]; then + repeatable_command+=" --runtime "\""dotnet"\""" + elif [[ "$runtime" == "aspnetcore" ]]; then + repeatable_command+=" --runtime "\""aspnetcore"\""" + fi + + repeatable_command+="$non_dynamic_parameters" + + if [ -n "$feed_credential" ]; then + repeatable_command+=" --feed-credential "\"""\""" + fi + + say "Repeatable invocation: $repeatable_command" +} + +calculate_vars() { + eval $invocation + + script_name=$(basename "$0") + normalized_architecture="$(get_normalized_architecture_from_architecture "$architecture")" + say_verbose "Normalized architecture: '$normalized_architecture'." + normalized_os="$(get_normalized_os "$user_defined_os")" + say_verbose "Normalized OS: '$normalized_os'." + normalized_quality="$(get_normalized_quality "$quality")" + say_verbose "Normalized quality: '$normalized_quality'." + normalized_channel="$(get_normalized_channel "$channel")" + say_verbose "Normalized channel: '$normalized_channel'." + normalized_product="$(get_normalized_product "$runtime")" + say_verbose "Normalized product: '$normalized_product'." + install_root="$(resolve_installation_path "$install_dir")" + say_verbose "InstallRoot: '$install_root'." + + normalized_architecture="$(get_normalized_architecture_for_specific_sdk_version "$version" "$normalized_channel" "$normalized_architecture")" + + if [[ "$runtime" == "dotnet" ]]; then + asset_relative_path="shared/Microsoft.NETCore.App" + asset_name=".NET Core Runtime" + elif [[ "$runtime" == "aspnetcore" ]]; then + asset_relative_path="shared/Microsoft.AspNetCore.App" + asset_name="ASP.NET Core Runtime" + elif [ -z "$runtime" ]; then + asset_relative_path="sdk" + asset_name=".NET Core SDK" + fi + + get_feeds_to_use +} + +install_dotnet() { + eval $invocation + local download_failed=false + local download_completed=false + local remote_file_size=0 + + mkdir -p "$install_root" + zip_path="${zip_path:-$(mktemp "$temporary_file_template")}" + say_verbose "Archive path: $zip_path" + + for link_index in "${!download_links[@]}" + do + download_link="${download_links[$link_index]}" + specific_version="${specific_versions[$link_index]}" + effective_version="${effective_versions[$link_index]}" + link_type="${link_types[$link_index]}" + + say "Attempting to download using $link_type link $download_link" + + # The download function will set variables $http_code and $download_error_msg in case of failure. + download_failed=false + download "$download_link" "$zip_path" 2>&1 || download_failed=true + + if [ "$download_failed" = true ]; then + case ${http_code-} in + 404) + say "The resource at $link_type link '$download_link' is not available." + ;; + *) + say "Failed to download $link_type link '$download_link': ${http_code-} ${download_error_msg-}" + ;; + esac + rm -f "$zip_path" 2>&1 && say_verbose "Temporary archive file $zip_path was removed" + else + download_completed=true + break + fi + done + + if [[ "$download_completed" == false ]]; then + say_err "Could not find \`$asset_name\` with version = $specific_version" + say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support" + return 1 + fi + + remote_file_size="$(get_remote_file_size "$download_link")" + + say "Extracting archive from $download_link" + extract_dotnet_package "$zip_path" "$install_root" "$remote_file_size" || return 1 + + # Check if the SDK version is installed; if not, fail the installation. + # if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed. + if [[ $specific_version == *"rtm"* || $specific_version == *"servicing"* ]]; then + IFS='-' + read -ra verArr <<< "$specific_version" + release_version="${verArr[0]}" + unset IFS; + say_verbose "Checking installation: version = $release_version" + if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$release_version"; then + say "Installed version is $effective_version" + return 0 + fi + fi + + # Check if the standard SDK version is installed. + say_verbose "Checking installation: version = $effective_version" + if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "Installed version is $effective_version" + return 0 + fi + + # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm. + say_err "Failed to verify the version of installed \`$asset_name\`.\nInstallation source: $download_link.\nInstallation location: $install_root.\nReport the bug at https://github.com/dotnet/install-scripts/issues." + say_err "\`$asset_name\` with version = $effective_version failed to install with an error." + return 1 +} + +args=("$@") + +local_version_file_relative_path="/.version" +bin_folder_relative_path="" +temporary_file_template="${TMPDIR:-/tmp}/dotnet.XXXXXXXXX" + +channel="LTS" +version="Latest" +json_file="" +install_dir="" +architecture="" +dry_run=false +no_path=false +azure_feed="" +uncached_feed="" +feed_credential="" +verbose=false +runtime="" +runtime_id="" +quality="" +internal=false +override_non_versioned_files=true +non_dynamic_parameters="" +user_defined_os="" + +while [ $# -ne 0 ] +do + name="$1" + case "$name" in + -c|--channel|-[Cc]hannel) + shift + channel="$1" + ;; + -v|--version|-[Vv]ersion) + shift + version="$1" + ;; + -q|--quality|-[Qq]uality) + shift + quality="$1" + ;; + --internal|-[Ii]nternal) + internal=true + non_dynamic_parameters+=" $name" + ;; + -i|--install-dir|-[Ii]nstall[Dd]ir) + shift + install_dir="$1" + ;; + --arch|--architecture|-[Aa]rch|-[Aa]rchitecture) + shift + architecture="$1" + ;; + --os|-[Oo][SS]) + shift + user_defined_os="$1" + ;; + --shared-runtime|-[Ss]hared[Rr]untime) + say_warning "The --shared-runtime flag is obsolete and may be removed in a future version of this script. The recommended usage is to specify '--runtime dotnet'." + if [ -z "$runtime" ]; then + runtime="dotnet" + fi + ;; + --runtime|-[Rr]untime) + shift + runtime="$1" + if [[ "$runtime" != "dotnet" ]] && [[ "$runtime" != "aspnetcore" ]]; then + say_err "Unsupported value for --runtime: '$1'. Valid values are 'dotnet' and 'aspnetcore'." + if [[ "$runtime" == "windowsdesktop" ]]; then + say_err "WindowsDesktop archives are manufactured for Windows platforms only." + fi + exit 1 + fi + ;; + --dry-run|-[Dd]ry[Rr]un) + dry_run=true + ;; + --no-path|-[Nn]o[Pp]ath) + no_path=true + non_dynamic_parameters+=" $name" + ;; + --verbose|-[Vv]erbose) + verbose=true + non_dynamic_parameters+=" $name" + ;; + --azure-feed|-[Aa]zure[Ff]eed) + shift + azure_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --uncached-feed|-[Uu]ncached[Ff]eed) + shift + uncached_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --feed-credential|-[Ff]eed[Cc]redential) + shift + feed_credential="$1" + #feed_credential should start with "?", for it to be added to the end of the link. + #adding "?" at the beginning of the feed_credential if needed. + [[ -z "$(echo $feed_credential)" ]] || [[ $feed_credential == \?* ]] || feed_credential="?$feed_credential" + ;; + --runtime-id|-[Rr]untime[Ii]d) + shift + runtime_id="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + say_warning "Use of --runtime-id is obsolete and should be limited to the versions below 2.1. To override architecture, use --architecture option instead. To override OS, use --os option instead." + ;; + --jsonfile|-[Jj][Ss]on[Ff]ile) + shift + json_file="$1" + ;; + --skip-non-versioned-files|-[Ss]kip[Nn]on[Vv]ersioned[Ff]iles) + override_non_versioned_files=false + non_dynamic_parameters+=" $name" + ;; + --keep-zip|-[Kk]eep[Zz]ip) + keep_zip=true + non_dynamic_parameters+=" $name" + ;; + --zip-path|-[Zz]ip[Pp]ath) + shift + zip_path="$1" + ;; + -?|--?|-h|--help|-[Hh]elp) + script_name="dotnet-install.sh" + echo ".NET Tools Installer" + echo "Usage:" + echo " # Install a .NET SDK of a given Quality from a given Channel" + echo " $script_name [-c|--channel ] [-q|--quality ]" + echo " # Install a .NET SDK of a specific public version" + echo " $script_name [-v|--version ]" + echo " $script_name -h|-?|--help" + echo "" + echo "$script_name is a simple command line interface for obtaining dotnet cli." + echo " Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" + echo " - The SDK needs to be installed without user interaction and without admin rights." + echo " - The SDK installation doesn't need to persist across multiple CI runs." + echo " To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer." + echo "" + echo "Options:" + echo " -c,--channel Download from the channel specified, Defaults to \`$channel\`." + echo " -Channel" + echo " Possible values:" + echo " - STS - the most recent Standard Term Support release" + echo " - LTS - the most recent Long Term Support release" + echo " - 2-part version in a format A.B - represents a specific release" + echo " examples: 2.0; 1.0" + echo " - 3-part version in a format A.B.Cxx - represents a specific SDK release" + echo " examples: 5.0.1xx, 5.0.2xx." + echo " Supported since 5.0 release" + echo " Warning: Value 'Current' is deprecated for the Channel parameter. Use 'STS' instead." + echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used." + echo " -v,--version Use specific VERSION, Defaults to \`$version\`." + echo " -Version" + echo " Possible values:" + echo " - latest - the latest build on specific channel" + echo " - 3-part version in a format A.B.C - represents specific version of build" + echo " examples: 2.0.0-preview2-006120; 1.1.0" + echo " -q,--quality Download the latest build of specified quality in the channel." + echo " -Quality" + echo " The possible values are: daily, preview, GA." + echo " Works only in combination with channel. Not applicable for STS and LTS channels and will be ignored if those channels are used." + echo " For SDK use channel in A.B.Cxx format. Using quality for SDK together with channel in A.B format is not supported." + echo " Supported since 5.0 release." + echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality." + echo " --internal,-Internal Download internal builds. Requires providing credentials via --feed-credential parameter." + echo " --feed-credential Token to access Azure feed. Used as a query string to append to the Azure feed." + echo " -FeedCredential This parameter typically is not specified." + echo " -i,--install-dir Install under specified location (see Install Location below)" + echo " -InstallDir" + echo " --architecture Architecture of dotnet binaries to be installed, Defaults to \`$architecture\`." + echo " --arch,-Architecture,-Arch" + echo " Possible values: x64, arm, arm64, s390x, ppc64le and loongarch64" + echo " --os Specifies operating system to be used when selecting the installer." + echo " Overrides the OS determination approach used by the script. Supported values: osx, linux, linux-musl, freebsd, rhel.6." + echo " In case any other value is provided, the platform will be determined by the script based on machine configuration." + echo " Not supported for legacy links. Use --runtime-id to specify platform for legacy links." + echo " Refer to: https://aka.ms/dotnet-os-lifecycle for more information." + echo " --runtime Installs a shared runtime only, without the SDK." + echo " -Runtime" + echo " Possible values:" + echo " - dotnet - the Microsoft.NETCore.App shared runtime" + echo " - aspnetcore - the Microsoft.AspNetCore.App shared runtime" + echo " --dry-run,-DryRun Do not perform installation. Display download link." + echo " --no-path, -NoPath Do not set PATH for the current process." + echo " --verbose,-Verbose Display diagnostics information." + echo " --azure-feed,-AzureFeed For internal use only." + echo " Allows using a different storage to download SDK archives from." + echo " --uncached-feed,-UncachedFeed For internal use only." + echo " Allows using a different storage to download SDK archives from." + echo " --skip-non-versioned-files Skips non-versioned files if they already exist, such as the dotnet executable." + echo " -SkipNonVersionedFiles" + echo " --jsonfile Determines the SDK version from a user specified global.json file." + echo " Note: global.json must have a value for 'SDK:Version'" + echo " --keep-zip,-KeepZip If set, downloaded file is kept." + echo " --zip-path, -ZipPath If set, downloaded file is stored at the specified path." + echo " -?,--?,-h,--help,-Help Shows this help message" + echo "" + echo "Install Location:" + echo " Location is chosen in following order:" + echo " - --install-dir option" + echo " - Environmental variable DOTNET_INSTALL_DIR" + echo " - $HOME/.dotnet" + exit 0 + ;; + *) + say_err "Unknown argument \`$name\`" + exit 1 + ;; + esac + + shift +done + +say_verbose "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" +say_verbose "- The SDK needs to be installed without user interaction and without admin rights." +say_verbose "- The SDK installation doesn't need to persist across multiple CI runs." +say_verbose "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.\n" + +if [ "$internal" = true ] && [ -z "$(echo $feed_credential)" ]; then + message="Provide credentials via --feed-credential parameter." + if [ "$dry_run" = true ]; then + say_warning "$message" + else + say_err "$message" + exit 1 + fi +fi + +check_min_reqs +calculate_vars +# generate_regular_links call below will 'exit' if the determined version is already installed. +generate_download_links + +if [[ "$dry_run" = true ]]; then + print_dry_run + exit 0 +fi + +install_dotnet + +bin_path="$(get_absolute_path "$(combine_paths "$install_root" "$bin_folder_relative_path")")" +if [ "$no_path" = false ]; then + say "Adding to current process PATH: \`$bin_path\`. Note: This change will be visible only when sourcing script." + export PATH="$bin_path":"$PATH" +else + say "Binaries of dotnet can be found in $bin_path" +fi + +say "Note that the script does not resolve dependencies during installation." +say "To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the \"Dependencies\" section." +say "Installation finished successfully." From bac6223e7e7b1066b04017036f04da35b8263540 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 8 Jul 2026 21:03:54 -0600 Subject: [PATCH 42/48] Restore missing VSInsertion artifact download task --- azure-pipelines/vs-insertion.yml | 3 +++ azure-pipelines/vs-validation.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index edb114c..9f5743a 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -69,6 +69,9 @@ extends: steps: - checkout: none - template: azure-pipelines/release-deployment-prep.yml@self + - download: CI + artifact: VSInsertion-Windows + displayName: ๐Ÿ”ป Download VSInsertion-Windows artifact - task: MicroBuildInsertVsPayload@5 displayName: ๐Ÿญ Insert VS Payload inputs: diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml index 3f2a194..cbfeb30 100644 --- a/azure-pipelines/vs-validation.yml +++ b/azure-pipelines/vs-validation.yml @@ -111,6 +111,9 @@ extends: displayName: ๐Ÿ”ป Download Variables-Windows artifact - powershell: $(Pipeline.Workspace)/Variables-Windows/_define.ps1 displayName: โš™๏ธ Set pipeline variables based on artifacts + - download: CI + artifact: VSInsertion-Windows + displayName: ๐Ÿ”ป Download VSInsertion-Windows artifact - task: MicroBuildInsertVsPayload@5 displayName: ๐Ÿญ Insert VS Payload inputs: From 6b93b0dabf2f09033e865eef65c42f4b411cc02b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 07:38:30 -0600 Subject: [PATCH 43/48] Update Microsoft Testing Platform to 2.3.1 (539) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ca1e17d..11bef29 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true true - 2.3.0 + 2.3.1 From 1573a0b176ab7b0e67df7f75843086abcc39cc49 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 9 Jul 2026 07:43:58 -0600 Subject: [PATCH 44/48] Fix vs-validation pipeline's artifact download task --- azure-pipelines/vs-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml index cbfeb30..e7e4c97 100644 --- a/azure-pipelines/vs-validation.yml +++ b/azure-pipelines/vs-validation.yml @@ -111,7 +111,7 @@ extends: displayName: ๐Ÿ”ป Download Variables-Windows artifact - powershell: $(Pipeline.Workspace)/Variables-Windows/_define.ps1 displayName: โš™๏ธ Set pipeline variables based on artifacts - - download: CI + - download: current artifact: VSInsertion-Windows displayName: ๐Ÿ”ป Download VSInsertion-Windows artifact - task: MicroBuildInsertVsPayload@5 From c98e75f8d6ab8eae749ff67d844d11b6d8bc1cde Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 10 Jul 2026 07:43:10 -0600 Subject: [PATCH 45/48] Reapply update to dotnet-coverage 18.9.0 (536) --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 2ddce27..9484456 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "dotnet-coverage": { - "version": "18.8.0", + "version": "18.9.0", "commands": [ "dotnet-coverage" ], From 327eaa2959ff41f3d16a6e391f5f583f6d9154f9 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 11 Jul 2026 11:27:41 -0600 Subject: [PATCH 46/48] Move AI files to a more generic place This allows other AI providers (e.g. Grok Build) to find the AI instruction and skill files. --- {.github => .agents}/skills/bundle-dependency-prs/SKILL.md | 0 {.github => .agents}/skills/update-library-template/SKILL.md | 0 .../skills/update-library-template/template-release-notes.md | 0 .github/copilot-instructions.md => AGENTS.md | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {.github => .agents}/skills/bundle-dependency-prs/SKILL.md (100%) rename {.github => .agents}/skills/update-library-template/SKILL.md (100%) rename {.github => .agents}/skills/update-library-template/template-release-notes.md (100%) rename .github/copilot-instructions.md => AGENTS.md (100%) diff --git a/.github/skills/bundle-dependency-prs/SKILL.md b/.agents/skills/bundle-dependency-prs/SKILL.md similarity index 100% rename from .github/skills/bundle-dependency-prs/SKILL.md rename to .agents/skills/bundle-dependency-prs/SKILL.md diff --git a/.github/skills/update-library-template/SKILL.md b/.agents/skills/update-library-template/SKILL.md similarity index 100% rename from .github/skills/update-library-template/SKILL.md rename to .agents/skills/update-library-template/SKILL.md diff --git a/.github/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md similarity index 100% rename from .github/skills/update-library-template/template-release-notes.md rename to .agents/skills/update-library-template/template-release-notes.md diff --git a/.github/copilot-instructions.md b/AGENTS.md similarity index 100% rename from .github/copilot-instructions.md rename to AGENTS.md From 1adc880ec6a0856656d36c2a44e7bbdd81b0b26a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:06:36 +0000 Subject: [PATCH 47/48] Update Microsoft Testing Platform to 2.3.2 (#541) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 11bef29..6280399 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true true - 2.3.1 + 2.3.2 From c3b5b129e071551fb8f8dd06da6cb5213ab37d0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:41:05 +0000 Subject: [PATCH 48/48] Bump .NET SDK to v10.0.302 (#542) * Update mcr.microsoft.com/dotnet/sdk Docker tag to v10.0.302 * Bump to .NET SDK 10.0.302 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Andrew Arnott --- .devcontainer/Dockerfile | 2 +- global.json | 2 +- tools/dotnet-install.sh | 0 3 files changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 tools/dotnet-install.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 635ed8c..f18d31b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions -FROM mcr.microsoft.com/dotnet/sdk:10.0.301@sha256:ea8bde36c11b6e7eec2656d0e59101d4462f6bd630730f2c8201ed0572b295d5 +FROM mcr.microsoft.com/dotnet/sdk:10.0.302@sha256:ed034a8bf0b24ded0cbbac07e17825d8e9ebfe21e308191d0f7421eaf5ad4664 # Installing mono makes `dotnet test` work without errors even for net472. # But installing it takes a long time, so it's excluded by default. diff --git a/global.json b/global.json index 4c9fb87..dba80e9 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.301", + "version": "10.0.302", "rollForward": "patch", "allowPrerelease": false }, diff --git a/tools/dotnet-install.sh b/tools/dotnet-install.sh old mode 100644 new mode 100755