From 6a4e43cebdecb716b377a17152569e1ccc79b832 Mon Sep 17 00:00:00 2001 From: "Pierson Lee (PIE)" Date: Mon, 13 Oct 2025 15:32:53 -0700 Subject: [PATCH 001/115] 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 002/115] 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 003/115] 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 004/115] 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 005/115] 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 006/115] 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 007/115] 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 008/115] 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 009/115] 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 010/115] 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 011/115] 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 012/115] 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 013/115] 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 014/115] 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 015/115] 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 016/115] 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 017/115] 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 018/115] 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 019/115] 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 020/115] 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 021/115] 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 022/115] 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 023/115] 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 024/115] 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 025/115] 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 026/115] 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 027/115] 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 028/115] 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 029/115] 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 030/115] 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 031/115] 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 032/115] 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 033/115] 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 034/115] 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 035/115] 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 036/115] 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 037/115] 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 038/115] 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 039/115] 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 040/115] 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 041/115] 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 042/115] 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 043/115] 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 044/115] 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 045/115] 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 046/115] 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 047/115] 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 048/115] 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 From ce33bc6811c353a24a5a2d9acd10a863fc8f7f62 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:15:38 -0600 Subject: [PATCH 049/115] Update actions/checkout action to v7.0.1 (543) 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 94a48d7..3caef37 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: - windows-2025 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - 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 c0436e8..71adf9e 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 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 9729dc3..c5a2fef 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 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 dfd3f57..af1a9ee 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. From bf268cb04393c5b92ff2f66d7cf63009b8231a1b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 01:22:55 +0000 Subject: [PATCH 050/115] Update dependency powershell to v7.6.4 (544) * Update actions/checkout action to v7.0.1 (#543) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency powershell to v7.6.4 --------- 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 30cea5a..56c4ec6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "powershell": { - "version": "7.6.3", + "version": "7.6.4", "commands": [ "pwsh" ], From 2b55315a6795ed98954bbace04118df4f1537691 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 21 Jul 2026 14:26:54 -0600 Subject: [PATCH 051/115] Update dependency dotnet-symbol to v10.0.737001 --- .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 30cea5a..ac392d0 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -38,7 +38,7 @@ "rollForward": false }, "dotnet-symbol": { - "version": "9.0.661903", + "version": "10.0.737001", "commands": [ "dotnet-symbol" ], From e3787d5a4f04fdc9a420a678e16a71c2ec665766 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 22 Jul 2026 20:56:38 -0600 Subject: [PATCH 052/115] Downgrade dotnet-symbol back to 9.0.661903 This is the latest available from `msft_consumption_public`. --- .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 1a1af61..56c4ec6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -38,7 +38,7 @@ "rollForward": false }, "dotnet-symbol": { - "version": "10.0.737001", + "version": "9.0.661903", "commands": [ "dotnet-symbol" ], From fa8b3ed472d8a339d9ab822323dfc116c990952f Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 24 Jul 2026 17:02:43 -0600 Subject: [PATCH 053/115] Avoid duplicate test dump artifacts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/artifacts/testResults.ps1 | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/artifacts/testResults.ps1 b/tools/artifacts/testResults.ps1 index a841967..b988010 100644 --- a/tools/artifacts/testResults.ps1 +++ b/tools/artifacts/testResults.ps1 @@ -6,12 +6,19 @@ $result = @{} $RepoRoot = Resolve-Path "$PSScriptRoot\..\.." $testRoot = Join-Path $RepoRoot test -$result[$testRoot] = (Get-ChildItem "$testRoot\TestResults" -Recurse -Directory | Get-ChildItem -Recurse -File) +$legacyTestResults = Join-Path $testRoot TestResults +if (Test-Path $legacyTestResults) { + $result[$testRoot] = Get-ChildItem $legacyTestResults -Recurse -Directory | + Get-ChildItem -Recurse -File | + Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '\\In\\' } +} $artifactStaging = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" $testlogsPath = Join-Path $artifactStaging "test_logs" if (Test-Path $testlogsPath) { - $result[$testlogsPath] = Get-ChildItem $testlogsPath -Recurse; + # Hang and crash dumps are copied into the TRX attachment directory. + $result[$testlogsPath] = Get-ChildItem $testlogsPath -Recurse | + Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '\\In\\' } } $result From 6dc5571c909006fe3be4e22341630249dd4856e0 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sun, 26 Jul 2026 18:34:21 -0600 Subject: [PATCH 054/115] Apply `-NoNuGetCredProvider` consistently This will avoid github API rate throttling for OSS repos that use nuget.org as their package feed. --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3caef37..3e7e2f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: โš™ Install prerequisites run: | - ./init.ps1 -UpgradePrerequisites + ./init.ps1 -UpgradePrerequisites -NoNuGetCredProvider dotnet --info # Print mono version if it is present. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c5a2fef..c462a08 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: โš™ Install prerequisites - run: ./init.ps1 -UpgradePrerequisites + run: ./init.ps1 -UpgradePrerequisites -NoNuGetCredProvider - run: dotnet docfx docfx/docfx.json name: ๐Ÿ“š Generate documentation From 77c904f800010dfd5fb0e5600784116a365cccb5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:30:45 -0600 Subject: [PATCH 055/115] Update Microsoft Testing Platform to 2.3.3 (545) 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 6280399..d026d2e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true true - 2.3.2 + 2.3.3 From abe5a3b63cdc254023208161484f94f4edb723f3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Jul 2026 10:56:17 -0700 Subject: [PATCH 056/115] Fix non-Windows path matching --- tools/artifacts/testResults.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/artifacts/testResults.ps1 b/tools/artifacts/testResults.ps1 index b988010..1817ca0 100644 --- a/tools/artifacts/testResults.ps1 +++ b/tools/artifacts/testResults.ps1 @@ -10,7 +10,7 @@ $legacyTestResults = Join-Path $testRoot TestResults if (Test-Path $legacyTestResults) { $result[$testRoot] = Get-ChildItem $legacyTestResults -Recurse -Directory | Get-ChildItem -Recurse -File | - Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '\\In\\' } + Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '[/\\]In[/\\]' } } $artifactStaging = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" @@ -18,7 +18,7 @@ $testlogsPath = Join-Path $artifactStaging "test_logs" if (Test-Path $testlogsPath) { # Hang and crash dumps are copied into the TRX attachment directory. $result[$testlogsPath] = Get-ChildItem $testlogsPath -Recurse | - Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '\\In\\' } + Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '[/\\]In[/\\]' } } $result From 50d20e2ea47840554a0f813559b81a05331aaac9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 08:09:56 -0600 Subject: [PATCH 057/115] Update mcr.microsoft.com/dotnet/sdk:10.0.302 Docker digest to 72dd743 (546) 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 f18d31b..6d2dbf4 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.302@sha256:ed034a8bf0b24ded0cbbac07e17825d8e9ebfe21e308191d0f7421eaf5ad4664 +FROM mcr.microsoft.com/dotnet/sdk:10.0.302@sha256:72dd743782f2ae7e5476fd64f6a460045e3998dc862218b80e6944cba79a01b0 # 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 e3025610dec6c749d42230c4882d50bdea92bd25 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 20:09:10 -0600 Subject: [PATCH 058/115] Fix Codecov CLI publishing Migrate coverage uploads to the supported Codecov CLI, verify its signed download, and propagate uploader failures to CI. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 1 - tools/Get-CodeCovTool.ps1 | 17 ++++++++++++----- tools/publish-CodeCov.ps1 | 23 ++++++++++++++++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e7e2f0..64a106f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,6 @@ jobs: } shell: pwsh timeout-minutes: 3 - continue-on-error: true docs: name: ๐Ÿ“ƒ Docs diff --git a/tools/Get-CodeCovTool.ps1 b/tools/Get-CodeCovTool.ps1 index 734ee60..893a353 100644 --- a/tools/Get-CodeCovTool.ps1 +++ b/tools/Get-CodeCovTool.ps1 @@ -10,15 +10,15 @@ Param( ) if ($IsMacOS) { - $codeCovUrl = "https://uploader.codecov.io/latest/macos/codecov" + $codeCovUrl = "https://cli.codecov.io/latest/macos/codecov" $toolName = 'codecov' } elseif ($IsLinux) { - $codeCovUrl = "https://uploader.codecov.io/latest/linux/codecov" + $codeCovUrl = "https://cli.codecov.io/latest/linux/codecov" $toolName = 'codecov' } else { - $codeCovUrl = "https://uploader.codecov.io/latest/windows/codecov.exe" + $codeCovUrl = "https://cli.codecov.io/latest/windows/codecov.exe" $toolName = 'codecov.exe' } @@ -41,7 +41,7 @@ Function Get-FileFromWeb([Uri]$Uri, $OutDir) { } $toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" -$binaryToolsPath = Join-Path $toolsPath codecov +$binaryToolsPath = Join-Path $toolsPath codecov-cli $testingPath = Join-Path $binaryToolsPath unverified $finalToolPath = Join-Path $binaryToolsPath $toolName @@ -52,13 +52,20 @@ if (!(Test-Path $finalToolPath)) { $tool = Get-FileFromWeb $codeCovUrl $testingPath $sha = Get-FileFromWeb "$codeCovUrl$shaSuffix" $testingPath $sig = Get-FileFromWeb "$codeCovUrl$sigSuffix" $testingPath - $key = Get-FileFromWeb https://keybase.io/codecovsecurity/pgp_keys.asc $testingPath + $key = Get-FileFromWeb https://keybase.io/codecovsecops/pgp_keys.asc $testingPath if ((Get-Command gpg -ErrorAction SilentlyContinue)) { Write-Host "Importing codecov key" -ForegroundColor Yellow gpg --import $key + if ($LASTEXITCODE -ne 0) { + throw "Failed to import the Codecov signature verification key." + } + Write-Host "Verifying signature on codecov hash" -ForegroundColor Yellow gpg --verify $sig $sha + if ($LASTEXITCODE -ne 0) { + throw "Codecov CLI signature verification failed." + } } else { if ($AllowSkipVerify) { Write-Warning "gpg not found. Unable to verify hash signature." diff --git a/tools/publish-CodeCov.ps1 b/tools/publish-CodeCov.ps1 index 1d73651..2f9b85a 100644 --- a/tools/publish-CodeCov.ps1 +++ b/tools/publish-CodeCov.ps1 @@ -21,10 +21,31 @@ Param ( ) $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path +$codeCovTool = & "$PSScriptRoot/Get-CodeCovTool.ps1" Get-ChildItem -Recurse -LiteralPath $PathToCodeCoverage -Filter "*.cobertura.xml" | % { $relativeFilePath = Resolve-Path -relative $_.FullName Write-Host "Uploading: $relativeFilePath" -ForegroundColor Yellow - & (& "$PSScriptRoot/Get-CodeCovTool.ps1") -t $CodeCovToken -f $relativeFilePath -R $RepoRoot -F $Flags -n $Name + $arguments = @( + "upload-process", + "--disable-search", + "--fail-on-error", + "-t", $CodeCovToken, + "-f", $relativeFilePath, + "--network-root-folder", $RepoRoot + ) + if ($Flags) { + $arguments += @("-F", $Flags) + } + + if ($Name) { + $arguments += @("-n", $Name) + } + + & $codeCovTool $arguments + if ($LASTEXITCODE -ne 0) { + Write-Error "Codecov CLI failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE + } } From 652e601775c091c01a0b904cb309590f3cf45119 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 10 Aug 2026 10:53:02 -0600 Subject: [PATCH 059/115] Add -NoCoverage switch and improve crash diagnostics Adds a -NoCoverage switch to dotnet-test-cloud.ps1 for both the MTP and VSTest code paths. Code coverage instrumentation can destabilize a test run, and when it does there is currently no way for a repo to keep running tests on the affected leg without hand-editing this script. Also collect a crash report alongside the crash dump, and restrict the dump to the heap. On Linux the native crash report is often the only way to identify the faulting thread and instruction when a test host dies of an access violation, and a heap dump is considerably smaller than the default full dump while still containing everything needed to analyze managed state. Finally, stop discarding crash dumps that have no TRX attachment copy. The previous filter kept a .dmp only when it appeared under a TRX 'In' directory, so on GitHub Actions -- which does not request a TRX report -- every crash dump was dropped from the uploaded artifacts, making test host crashes impossible to diagnose. The dumps are now de-duplicated by file name instead, which still avoids uploading the same dump twice on Azure Pipelines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/artifacts/testResults.ps1 | 16 ++++++++++------ tools/dotnet-test-cloud.ps1 | 31 +++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/tools/artifacts/testResults.ps1 b/tools/artifacts/testResults.ps1 index 1817ca0..d96c884 100644 --- a/tools/artifacts/testResults.ps1 +++ b/tools/artifacts/testResults.ps1 @@ -4,21 +4,25 @@ Param( $result = @{} +# Hang and crash dumps are copied into the TRX attachment directory when a TRX report is requested. +# Prefer that copy to avoid uploading the same dump twice, but keep the original when no attachment +# copy exists (e.g. GitHub Actions runs, which don't request a TRX report) so crashes stay diagnosable. +function Select-TestResultFile($files) { + $attachedDumpNames = @($files | Where-Object { $_.Extension -eq '.dmp' -and $_.FullName -match '[/\\]In[/\\]' } | ForEach-Object { $_.Name }) + $files | Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '[/\\]In[/\\]' -or $attachedDumpNames -notcontains $_.Name } +} + $RepoRoot = Resolve-Path "$PSScriptRoot\..\.." $testRoot = Join-Path $RepoRoot test $legacyTestResults = Join-Path $testRoot TestResults if (Test-Path $legacyTestResults) { - $result[$testRoot] = Get-ChildItem $legacyTestResults -Recurse -Directory | - Get-ChildItem -Recurse -File | - Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '[/\\]In[/\\]' } + $result[$testRoot] = Select-TestResultFile @(Get-ChildItem $legacyTestResults -Recurse -Directory | Get-ChildItem -Recurse -File) } $artifactStaging = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" $testlogsPath = Join-Path $artifactStaging "test_logs" if (Test-Path $testlogsPath) { - # Hang and crash dumps are copied into the TRX attachment directory. - $result[$testlogsPath] = Get-ChildItem $testlogsPath -Recurse | - Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '[/\\]In[/\\]' } + $result[$testlogsPath] = Select-TestResultFile @(Get-ChildItem $testlogsPath -Recurse) } $result diff --git a/tools/dotnet-test-cloud.ps1 b/tools/dotnet-test-cloud.ps1 index 5d20481..da876cb 100644 --- a/tools/dotnet-test-cloud.ps1 +++ b/tools/dotnet-test-cloud.ps1 @@ -13,6 +13,8 @@ A switch to run the tests in an x86 process. .PARAMETER dotnet32 The path to a 32-bit dotnet executable to use. +.PARAMETER NoCoverage + A switch to skip code coverage collection. #> [CmdletBinding()] Param( @@ -20,7 +22,8 @@ Param( [string]$Agent='Local', [switch]$PublishResults, [switch]$x86, - [string]$dotnet32 + [string]$dotnet32, + [switch]$NoCoverage ) $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path @@ -60,10 +63,12 @@ if ($isMTP) { ,'--hangdump' ,'--hangdump-timeout','120s' ,'--crashdump' + ,'--crashdump-type','Heap' + # The native crash report accompanies the dump and is often the only way to identify the + # faulting thread and instruction when a test host dies of an access violation on Linux. + ,'--crash-report-if-supported' ) $mtpArgs = @( - ,'--coverage' - ,'--coverage-output-format','cobertura' ,'--diagnostic' ,'--diagnostic-output-directory',$testLogs ,'--diagnostic-verbosity','Information' @@ -71,12 +76,19 @@ if ($isMTP) { ,'--report-trx' ) + if (-not $NoCoverage) { + $mtpArgs += @( + ,'--coverage' + ,'--coverage-output-format','cobertura' + ,'--coverage-settings',"$PSScriptRoot/test.runsettings" + ) + } + & $dotnet test --solution $RepoRoot ` --no-build ` -c $Configuration ` -bl:"$testBinLog" ` --filter-not-trait 'TestCategory=FailsInCloudTest' ` - --coverage-settings "$PSScriptRoot/test.runsettings" ` @mtpArgs ` @dumpSwitches ` @extraArgs @@ -85,17 +97,24 @@ if ($isMTP) { $trxFiles = Get-ChildItem -Recurse -Path $testLogs\*.trx } else { $testDiagLog = Join-Path $ArtifactStagingFolder (Join-Path test_logs diag.log) + $coverageArgs = @() + if (-not $NoCoverage) { + $coverageArgs = @( + ,'--collect','Code Coverage;Format=cobertura' + ,'--settings',"$PSScriptRoot/test.runsettings" + ) + } + & $dotnet test $RepoRoot ` --no-build ` -c $Configuration ` --filter "TestCategory!=FailsInCloudTest" ` - --collect "Code Coverage;Format=cobertura" ` - --settings "$PSScriptRoot/test.runsettings" ` --blame-hang-timeout 60s ` --blame-crash ` -bl:"$testBinLog" ` --diag "$testDiagLog;TraceLevel=info" ` --logger trx ` + @coverageArgs ` @extraArgs if ($LASTEXITCODE -ne 0) { $failedTests += 1 } From 75156e65d333855aed451c9f5364dede3c054989 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:28:15 -0600 Subject: [PATCH 060/115] Update Dockerfile and global.json updates to v10.0.400 (549) --- .devcontainer/Dockerfile | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6d2dbf4..cfa41d1 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.302@sha256:72dd743782f2ae7e5476fd64f6a460045e3998dc862218b80e6944cba79a01b0 +FROM mcr.microsoft.com/dotnet/sdk:10.0.400@sha256:e1fc6e423f543119c406d24e2e687d67c569f18f04a37a8b0005d80ad0dcee80 # 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 dba80e9..d0cca71 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.302", + "version": "10.0.400", "rollForward": "patch", "allowPrerelease": false }, From c97c16860809aaeb0f9638e90479508dce85788e Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 11 Aug 2026 20:34:50 -0600 Subject: [PATCH 061/115] Fix .NET SDK installer script to not fail from a cached releases.json file --- tools/Install-DotNetSdk.ps1 | 57 ++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1 index bec37c9..2621dda 100644 --- a/tools/Install-DotNetSdk.ps1 +++ b/tools/Install-DotNetSdk.ps1 @@ -99,9 +99,9 @@ if (!$IncludeAspNetCore) { $aspnetRuntimeVersions = @() } -Function Get-FileFromWeb([Uri]$Uri, $OutDir) { +Function Get-FileFromWeb([Uri]$Uri, $OutDir, [switch]$Force) { $OutFile = Join-Path $OutDir $Uri.Segments[-1] - if (!(Test-Path $OutFile)) { + if ($Force -or !(Test-Path $OutFile)) { Write-Verbose "Downloading $Uri..." if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } try { @@ -134,38 +134,49 @@ Function Get-InstallerExe( $majorMinor = "$($TypedVersion.Major).$($TypedVersion.Minor)" $ReleasesFile = Join-Path $DotNetInstallScriptRoot "$majorMinor\releases.json" - if (!(Test-Path $ReleasesFile)) { - Get-FileFromWeb -Uri "https://builds.dotnet.microsoft.com/dotnet/release-metadata/$majorMinor/releases.json" -OutDir (Split-Path $ReleasesFile) | Out-Null + $ReleasesFileWasCached = Test-Path $ReleasesFile + $ReleasesUri = "https://builds.dotnet.microsoft.com/dotnet/release-metadata/$majorMinor/releases.json" + if (!$ReleasesFileWasCached) { + Get-FileFromWeb -Uri $ReleasesUri -OutDir (Split-Path $ReleasesFile) | Out-Null } - $releases = Get-Content $ReleasesFile | ConvertFrom-Json $url = $null - foreach ($release in $releases.releases) { - $filesElement = $null - if ($release.$sku.version -eq $Version) { - $filesElement = $release.$sku.files - } - if (!$filesElement -and ($sku -eq 'sdk') -and $release.sdks) { - foreach ($sdk in $release.sdks) { - if ($sdk.version -eq $Version) { - $filesElement = $sdk.files - break + for ($attempt = 0; $attempt -lt 2; $attempt++) { + $releases = Get-Content $ReleasesFile | ConvertFrom-Json + foreach ($release in $releases.releases) { + $filesElement = $null + if ($release.$sku.version -eq $Version) { + $filesElement = $release.$sku.files + } + if (!$filesElement -and ($sku -eq 'sdk') -and $release.sdks) { + foreach ($sdk in $release.sdks) { + if ($sdk.version -eq $Version) { + $filesElement = $sdk.files + break + } } } - } - if ($filesElement) { - foreach ($file in $filesElement) { - if ($file.rid -eq "win-$Architecture") { - $url = $file.url + if ($filesElement) { + foreach ($file in $filesElement) { + if ($file.rid -eq "win-$Architecture") { + $url = $file.url + Break + } + } + + if ($url) { Break } } + } - if ($url) { - Break - } + if ($url -or !$ReleasesFileWasCached -or $attempt -gt 0) { + break } + + Write-Verbose "Release not found in cached metadata. Refreshing $ReleasesUri..." + Get-FileFromWeb -Uri $ReleasesUri -OutDir (Split-Path $ReleasesFile) -Force | Out-Null } if ($url) { From 7d49764c1a6f13fd6512192dd3a9706531bb22ff Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 12 Aug 2026 06:36:02 -0600 Subject: [PATCH 062/115] Use ps1 splatting and exit instead of return --- tools/dotnet-test-cloud.ps1 | 2 +- tools/publish-CodeCov.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/dotnet-test-cloud.ps1 b/tools/dotnet-test-cloud.ps1 index da876cb..7ecaf89 100644 --- a/tools/dotnet-test-cloud.ps1 +++ b/tools/dotnet-test-cloud.ps1 @@ -43,7 +43,7 @@ if ($x86) { Write-Host "Running tests using `"$dotnet`"" -ForegroundColor DarkGray } else { Write-Error "Unable to find 32-bit dotnet.exe" - return 1 + exit 1 } } } diff --git a/tools/publish-CodeCov.ps1 b/tools/publish-CodeCov.ps1 index 2f9b85a..c393b7d 100644 --- a/tools/publish-CodeCov.ps1 +++ b/tools/publish-CodeCov.ps1 @@ -43,7 +43,7 @@ Get-ChildItem -Recurse -LiteralPath $PathToCodeCoverage -Filter "*.cobertura.xml $arguments += @("-n", $Name) } - & $codeCovTool $arguments + & $codeCovTool @arguments if ($LASTEXITCODE -ne 0) { Write-Error "Codecov CLI failed with exit code $LASTEXITCODE." exit $LASTEXITCODE From aed7260018d9c42121bf31efc74a778b1cf180ff Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 12 Aug 2026 09:44:29 -0600 Subject: [PATCH 063/115] Improve test and code coverage scripts * Correct MTP -- forwarding with portable solution discovery. * Increase effective hang timeout to five minutes. * Batch all Codecov reports into one CLI invocation. * Preserve no-op behavior when no coverage exists. --- tools/dotnet-test-cloud.ps1 | 11 +++++++-- tools/publish-CodeCov.ps1 | 48 ++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/tools/dotnet-test-cloud.ps1 b/tools/dotnet-test-cloud.ps1 index 7ecaf89..3d8d087 100644 --- a/tools/dotnet-test-cloud.ps1 +++ b/tools/dotnet-test-cloud.ps1 @@ -61,7 +61,7 @@ if ($isMTP) { $dumpSwitches = @( ,'--hangdump' - ,'--hangdump-timeout','120s' + ,'--hangdump-timeout','5m' ,'--crashdump' ,'--crashdump-type','Heap' # The native crash report accompanies the dump and is often the only way to identify the @@ -84,10 +84,17 @@ if ($isMTP) { ) } - & $dotnet test --solution $RepoRoot ` + $solutionFiles = @(Get-ChildItem -LiteralPath $RepoRoot -File | Where-Object { $_.Extension -in '.sln', '.slnx' }) + if ($solutionFiles.Count -ne 1) { + throw "Expected exactly one solution file in $RepoRoot, but found $($solutionFiles.Count)." + } + + $solutionPath = $solutionFiles[0].FullName + & $dotnet test $solutionPath ` --no-build ` -c $Configuration ` -bl:"$testBinLog" ` + -- ` --filter-not-trait 'TestCategory=FailsInCloudTest' ` @mtpArgs ` @dumpSwitches ` diff --git a/tools/publish-CodeCov.ps1 b/tools/publish-CodeCov.ps1 index c393b7d..d9bc81d 100644 --- a/tools/publish-CodeCov.ps1 +++ b/tools/publish-CodeCov.ps1 @@ -22,30 +22,34 @@ Param ( $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path $codeCovTool = & "$PSScriptRoot/Get-CodeCovTool.ps1" +$coverageFiles = @(Get-ChildItem -Recurse -LiteralPath $PathToCodeCoverage -Filter "*.cobertura.xml") +if ($coverageFiles.Count -eq 0) { + return +} -Get-ChildItem -Recurse -LiteralPath $PathToCodeCoverage -Filter "*.cobertura.xml" | % { - $relativeFilePath = Resolve-Path -relative $_.FullName - +$arguments = @( + "upload-process", + "--disable-search", + "--fail-on-error", + "-t", $CodeCovToken, + "--network-root-folder", $RepoRoot +) +foreach ($coverageFile in $coverageFiles) { + $relativeFilePath = Resolve-Path -Relative $coverageFile.FullName Write-Host "Uploading: $relativeFilePath" -ForegroundColor Yellow - $arguments = @( - "upload-process", - "--disable-search", - "--fail-on-error", - "-t", $CodeCovToken, - "-f", $relativeFilePath, - "--network-root-folder", $RepoRoot - ) - if ($Flags) { - $arguments += @("-F", $Flags) - } + $arguments += @("-f", $relativeFilePath) +} - if ($Name) { - $arguments += @("-n", $Name) - } +if ($Flags) { + $arguments += @("-F", $Flags) +} + +if ($Name) { + $arguments += @("-n", $Name) +} - & $codeCovTool @arguments - if ($LASTEXITCODE -ne 0) { - Write-Error "Codecov CLI failed with exit code $LASTEXITCODE." - exit $LASTEXITCODE - } +& $codeCovTool @arguments +if ($LASTEXITCODE -ne 0) { + Write-Error "Codecov CLI failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE } From 663bdc9d7b5438049d86bf5c350c11720732c0b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:32:24 -0600 Subject: [PATCH 064/115] Update dependency dotnet-coverage to v18.10.0 (#552) 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 56c4ec6..7b7805b 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.10.0", "commands": [ "dotnet-coverage" ], From 1264e442e0cf7a3357d05b47959912fec70545ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:32:38 -0600 Subject: [PATCH 065/115] Update dependency Microsoft.Testing.Extensions.CodeCoverage to 18.10.0 (#553) 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 d026d2e..462d1d0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + From a853c38c01e1d66c82ee918f1c6ca1dad1ed6b8a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 13 Aug 2026 10:08:58 -0600 Subject: [PATCH 066/115] Revert "Update dependency dotnet-coverage to v18.10.0 (552)" This reverts commit 663bdc9d7b5438049d86bf5c350c11720732c0b1. We can't update to this for another several days. --- .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 7b7805b..56c4ec6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "dotnet-coverage": { - "version": "18.10.0", + "version": "18.9.0", "commands": [ "dotnet-coverage" ], From 13a168ec06a99eae9bbce7b3ea2eb7875425aa19 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:31:08 -0600 Subject: [PATCH 067/115] Update dependency powershell to v7.6.5 (554) 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 7b7805b..636973a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "powershell": { - "version": "7.6.4", + "version": "7.6.5", "commands": [ "pwsh" ], From aff6973563477503fa4c5687eb2fb8ecee6e8f46 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:45:26 -0600 Subject: [PATCH 068/115] Update dependency xunit.v3.mtp-v2 to v4 (555) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 462d1d0..7dcdc19 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + From f5d9436d8989c3ccded6c85f65e53357ae2b0992 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:46:16 -0600 Subject: [PATCH 069/115] Update dependency nerdbank.dotnetrepotools to v1.5.42 (556) 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 636973a..3abeb5a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -31,7 +31,7 @@ "rollForward": false }, "nerdbank.dotnetrepotools": { - "version": "1.5.15", + "version": "1.5.42", "commands": [ "repo" ], From d96634554028fda7232c2ec2ce2ea9f3f1c49be7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:59:43 -0600 Subject: [PATCH 070/115] Update mcr.microsoft.com/dotnet/sdk:10.0.400 Docker digest to e1ffd2a (557) 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 cfa41d1..566f9e4 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.400@sha256:e1fc6e423f543119c406d24e2e687d67c569f18f04a37a8b0005d80ad0dcee80 +FROM mcr.microsoft.com/dotnet/sdk:10.0.400@sha256:e1ffd2a92ae84c1291bc1b6887501f8af98e6331e7af6d4c8d37168c5e87a64c # 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 38e8ff9ddfb56b895835c40e43fe13aafdcedba3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 20 Aug 2026 08:48:05 -0600 Subject: [PATCH 071/115] Update notes about how the release workflow works --- CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05e4d16..dae8d95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,8 +60,10 @@ Having previously used `nbgv tag` and pushing the tag will help you identify the After publishing the release, the `.github/workflows/release.yml` workflow will be automatically triggered, which will: 1. Find the most recent `.github/workflows/build.yml` GitHub workflow run of the tagged release. -1. Upload the `deployables` artifact from that workflow run to your GitHub Release. -1. If you have `NUGET_API_KEY` defined as a secret variable for your repo or org, any nuget packages in the `deployables` artifact will be pushed to nuget.org. +1. Upload the `deployables-Linux` artifact from that workflow run to your GitHub Release. +1. Any nuget packages in the `deployables-Linux` artifact will be pushed to nuget.org. + +The workflow is written to leverage NuGet.org Trusted Publishing. ### Azure Pipelines From 60fd229ac29504262000ecee51f1bf29d095babf Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 20 Aug 2026 08:49:27 -0600 Subject: [PATCH 072/115] Document the requirement for a repo secret --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dae8d95..2c566c0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,6 +64,7 @@ After publishing the release, the `.github/workflows/release.yml` workflow will 1. Any nuget packages in the `deployables-Linux` artifact will be pushed to nuget.org. The workflow is written to leverage NuGet.org Trusted Publishing. +You should set `NUGET_USER` as a repo secret to satisfy Trusted Publishing requirements. ### Azure Pipelines From 910d33cf9d4566d498e2944b58618ee4d0de6592 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 20 Aug 2026 08:57:50 -0600 Subject: [PATCH 073/115] Add better error and corner case handling to release workflow --- .github/workflows/release.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b13322..bbaf6be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,9 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + $ErrorActionPreference = 'Stop' + $PSNativeCommandUseErrorActionPreference = $true + if ('${{ inputs.ship_run_id }}') { $runid = '${{ inputs.ship_run_id }}' } else { @@ -40,8 +43,8 @@ jobs: Write-Host "Resolved ${{ github.ref_name }} to $commitSha" - $releases = gh run list -R ${{ github.repository }} -c $commitSha -w .github/workflows/build.yml -s success --json databaseId,startedAt,event,headBranch ` - | ConvertFrom-Json | Sort-Object startedAt -Descending + $releases = @(gh run list -R ${{ github.repository }} -c $commitSha -w .github/workflows/build.yml -s success --json databaseId,startedAt,event,headBranch ` + | ConvertFrom-Json | Sort-Object startedAt -Descending) $preferredReleases = $releases | Where-Object { $_.event -eq 'push' -and ($_.headBranch -eq 'main' -or $_.headBranch -match '^v\d+(?:\.\d+)?$') @@ -93,6 +96,9 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + $ErrorActionPreference = 'Stop' + $PSNativeCommandUseErrorActionPreference = $true + Get-ChildItem '${{ runner.temp }}/deployables' -File -Recurse |% { Write-Host "Uploading $($_.Name) to release..." gh release -R ${{ github.repository }} upload "${{ github.ref_name }}" $_.FullName From f700f34f85fbcdda1033a1d9013f8ee1c52812d3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 20 Aug 2026 11:12:12 -0600 Subject: [PATCH 074/115] Bump CSharpIsNullAnalyzer to 0.2.12 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7dcdc19..ffa3b43 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + From dff5d6eb0e7884ee94c024e1a81895bff7d0afcf Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 20 Aug 2026 14:22:47 -0600 Subject: [PATCH 075/115] Bump CSharpIsNullAnalyzer to 0.2.19 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ffa3b43..6d84493 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + From 549ad157543aad73d56bb43d93762ad337735070 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 24 Aug 2026 11:42:04 -0600 Subject: [PATCH 076/115] Update network isolation policy This latest update keeps compliant with the latest security push. --- 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, 7 insertions(+), 7 deletions(-) diff --git a/azure-pipelines/archive-sourcecode.yml b/azure-pipelines/archive-sourcecode.yml index cb8d68e..13015be 100644 --- a/azure-pipelines/archive-sourcecode.yml +++ b/azure-pipelines/archive-sourcecode.yml @@ -36,7 +36,7 @@ extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: settings: - networkIsolationPolicy: Permissive,CFSClean2 + networkIsolationPolicy: DefaultDeny,CFSClean3 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml index acbfbac..8b342de 100644 --- a/azure-pipelines/libtemplate-update.yml +++ b/azure-pipelines/libtemplate-update.yml @@ -31,7 +31,7 @@ extends: template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate parameters: settings: - networkIsolationPolicy: Permissive,CFSClean2 + networkIsolationPolicy: DefaultDeny,CFSClean3 sdl: sourceAnalysisPool: name: AzurePipelines-EO diff --git a/azure-pipelines/official.yml b/azure-pipelines/official.yml index 99815eb..084d0d8 100644 --- a/azure-pipelines/official.yml +++ b/azure-pipelines/official.yml @@ -46,7 +46,7 @@ extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: settings: - networkIsolationPolicy: Permissive,CFSClean2 + networkIsolationPolicy: DefaultDeny,CFSClean3 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable codeSignValidation: diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index a8218f0..3df977e 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -21,7 +21,7 @@ extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: settings: - networkIsolationPolicy: Permissive,CFSClean2 + networkIsolationPolicy: DefaultDeny,CFSClean3 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable diff --git a/azure-pipelines/unofficial.yml b/azure-pipelines/unofficial.yml index efc60f3..fd59379 100644 --- a/azure-pipelines/unofficial.yml +++ b/azure-pipelines/unofficial.yml @@ -56,7 +56,7 @@ extends: template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate parameters: settings: - networkIsolationPolicy: Permissive,CFSClean2 + networkIsolationPolicy: DefaultDeny,CFSClean3 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable credscan: diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index 9f5743a..1b5313e 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -23,7 +23,7 @@ extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: settings: - networkIsolationPolicy: Permissive,CFSClean2 + networkIsolationPolicy: DefaultDeny,CFSClean3 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable sbom: diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml index e7e4c97..b4037ee 100644 --- a/azure-pipelines/vs-validation.yml +++ b/azure-pipelines/vs-validation.yml @@ -27,7 +27,7 @@ extends: template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate parameters: settings: - networkIsolationPolicy: Permissive,CFSClean2 + networkIsolationPolicy: DefaultDeny,CFSClean3 sdl: sourceAnalysisPool: VSEng-MicroBuildVSStable credscan: From cf76312e1968f88766808c44540fb616b6744683 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 24 Aug 2026 12:10:53 -0600 Subject: [PATCH 077/115] Bump MicroBuild to 2.0.257 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c3d8ce3..f9ef4a9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ true 2.3.3 - 2.0.226 + 2.0.257 From f1c1a2d603e8b58352b6888e10b5c94e13f5f76d Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 24 Aug 2026 12:28:58 -0600 Subject: [PATCH 078/115] Drop GitHub Actions section --- CONTRIBUTING.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c566c0..2af8f46 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,20 +52,6 @@ Use `nbgv tag` to create a tag for a particular commit that you mean to release. Push the tag. -### GitHub Actions - -When your repo is hosted by GitHub and you are using GitHub Actions, you should create a GitHub Release using the standard GitHub UI. -Having previously used `nbgv tag` and pushing the tag will help you identify the precise commit and name to use for this release. - -After publishing the release, the `.github/workflows/release.yml` workflow will be automatically triggered, which will: - -1. Find the most recent `.github/workflows/build.yml` GitHub workflow run of the tagged release. -1. Upload the `deployables-Linux` artifact from that workflow run to your GitHub Release. -1. Any nuget packages in the `deployables-Linux` artifact will be pushed to nuget.org. - -The workflow is written to leverage NuGet.org Trusted Publishing. -You should set `NUGET_USER` as a repo secret to satisfy Trusted Publishing requirements. - ### Azure Pipelines When your repo builds with Azure Pipelines, use the `azure-pipelines/release.yml` pipeline. From 511e4ab198ac4fe85865c16832cbbc934d1c0f87 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 25 Aug 2026 07:15:03 -0600 Subject: [PATCH 079/115] Reapply "Update dependency dotnet-coverage to v18.10.0 (552)" This reverts commit a853c38c01e1d66c82ee918f1c6ca1dad1ed6b8a. --- .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 c6c3009..3abeb5a 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.10.0", "commands": [ "dotnet-coverage" ], From d15a649696015bb28ce9def257223aec178ab690 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 20:16:48 -0600 Subject: [PATCH 080/115] Update nbgv and nerdbank.gitversioning updates to 3.10.94 (558) 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 3abeb5a..097e92d 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "nbgv": { - "version": "3.10.91", + "version": "3.10.94", "commands": [ "nbgv" ], diff --git a/Directory.Packages.props b/Directory.Packages.props index 6d84493..8ad421c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - + From ca21dec40381a60886ad08e4fe1931bbfe413702 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:19:55 -0600 Subject: [PATCH 081/115] Update actions/deploy-pages action to v5.0.1 (559) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c462a08..c0492a9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -40,4 +40,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 From 5bfa11b7f268e690851d7e29ecd76a67003c12b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:27:36 +0000 Subject: [PATCH 082/115] Update dependency dotnet-coverage to v18.11.0 (560) 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 097e92d..db2bb85 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "dotnet-coverage": { - "version": "18.10.0", + "version": "18.11.0", "commands": [ "dotnet-coverage" ], From d660776b19ddfd8ea8549c74b43b4b0567caade3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:06:30 +0000 Subject: [PATCH 083/115] Update dependency Microsoft.Testing.Extensions.CodeCoverage to 18.11.0 (561) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8ad421c..bdf5637 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,13 +4,13 @@ true true - 2.3.3 + 2.4.0 - + From b8a8b51c17a7fcc4104b65377ba3c4e5967060e1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 06:21:30 -0600 Subject: [PATCH 084/115] Update dependency dotnet-symbol to v10 (562) 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 db2bb85..ad207b6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -38,7 +38,7 @@ "rollForward": false }, "dotnet-symbol": { - "version": "9.0.661903", + "version": "10.0.731102", "commands": [ "dotnet-symbol" ], From 916a18028aeb287efbd62492b2dd04819881a1aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:30:16 -0600 Subject: [PATCH 085/115] Update mcr.microsoft.com/dotnet/sdk:10.0.400 Docker digest to 4beef5b (563) --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 566f9e4..694e58b 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.400@sha256:e1ffd2a92ae84c1291bc1b6887501f8af98e6331e7af6d4c8d37168c5e87a64c +FROM mcr.microsoft.com/dotnet/sdk:10.0.400@sha256:4beef5b8919dcaa2dc924233bd069257e883cc7a061e09088a97d152d6a48510 # 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 44631b0077f847ac87c2d49c75438d9c35026cff Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 8 Sep 2026 15:14:24 -0600 Subject: [PATCH 086/115] Update dependency powershell to v7.6.6 --- .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 ad207b6..79a9fb7 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "powershell": { - "version": "7.6.5", + "version": "7.6.6", "commands": [ "pwsh" ], From 7e6372f6826cb84e5ad4d970719de0c54e22e935 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 8 Sep 2026 15:16:35 -0600 Subject: [PATCH 087/115] Bump .NET SDK to 10.0.401 --- .devcontainer/Dockerfile | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 694e58b..1b15368 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.400@sha256:4beef5b8919dcaa2dc924233bd069257e883cc7a061e09088a97d152d6a48510 +FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:096ca98743616ccfa14dccaa2a86615fd7345bd0c4535445226e98dbd46af2b5 # 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 d0cca71..8c833e4 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.400", + "version": "10.0.401", "rollForward": "patch", "allowPrerelease": false }, From efc103e433cabacce6fa34a0624ac296c69ba028 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 06:35:53 -0600 Subject: [PATCH 088/115] Update mcr.microsoft.com/dotnet/sdk:10.0.401 Docker digest to 4ea6fe7 (564) 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 1b15368..c9af5a4 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.401@sha256:096ca98743616ccfa14dccaa2a86615fd7345bd0c4535445226e98dbd46af2b5 +FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:4ea6fe75dd36706bb6d8c3c293d4c4315840f5d76ea28ac97def77e3ec487fa5 # 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 75371f2f8983dc2b91911f5473d56f9dee92b934 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:27:14 -0600 Subject: [PATCH 089/115] Update dependency dotnet-symbol to v10.0.745401 (565) 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 79a9fb7..9575f99 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -38,7 +38,7 @@ "rollForward": false }, "dotnet-symbol": { - "version": "10.0.731102", + "version": "10.0.745401", "commands": [ "dotnet-symbol" ], From 0fe44f9759bc1b317d9aa7f302b7381c4789e046 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:18:01 -0600 Subject: [PATCH 090/115] Update dependency dotnet-coverage to v18.11.2 (566) 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 9575f99..ac2120a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "dotnet-coverage": { - "version": "18.11.0", + "version": "18.11.2", "commands": [ "dotnet-coverage" ], From a66bb7e95d8093fa349e23debdc32b9823bbfb71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:18:06 -0600 Subject: [PATCH 091/115] Update dependency Microsoft.Testing.Extensions.CodeCoverage to 18.11.2 (567) 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 bdf5637..2c5250e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + From 30e9ed6e0c7341f8b66d8f17689a2e44156e7b8a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:18:11 -0600 Subject: [PATCH 092/115] Update mcr.microsoft.com/dotnet/sdk:10.0.401 Docker digest to 2fa828c (568) 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 c9af5a4..7fbd7f5 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.401@sha256:4ea6fe75dd36706bb6d8c3c293d4c4315840f5d76ea28ac97def77e3ec487fa5 +FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:2fa828c68761b1b8c23d7662dc134421b9d3b59fe1425fdbc80804e390cdb24d # 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 05c10db5ffbb89b053c245a4fdbc9e2438bcb7fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:18:16 -0600 Subject: [PATCH 093/115] Update dependency xunit.v3.mtp-v2 to 4.0.1 (569) 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 2c5250e..fd61faa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + From c8e16228c60776f2c9dbc83dbb7ea48c77d3452a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 15 Sep 2026 11:59:06 -0600 Subject: [PATCH 094/115] Migrate from `VSEngSS-MicroBuild2022-1ES` to `VSEng-MicroBuildVSStable` --- azure-pipelines/vs-insertion.yml | 2 +- azure-pipelines/vs-validation.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index 1b5313e..3f8a4d8 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -63,7 +63,7 @@ extends: - job: insert dependsOn: upload displayName: VS insertion - pool: VSEngSS-MicroBuild2022-1ES + pool: VSEng-MicroBuildVSStable templateContext: outputParentDirectory: $(Pipeline.Workspace)/CI steps: diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml index b4037ee..70e9c45 100644 --- a/azure-pipelines/vs-validation.yml +++ b/azure-pipelines/vs-validation.yml @@ -101,7 +101,7 @@ extends: - job: insertion displayName: VS insertion dependsOn: upload - pool: VSEngSS-MicroBuild2022-1ES + pool: VSEng-MicroBuildVSStable steps: - checkout: self clean: true From 334f592be750b33564ea741b30b2fa76b4056e28 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 16 Sep 2026 11:44:45 -0600 Subject: [PATCH 095/115] Pin Microsoft.Testing.Platform This is so that if xunit.v3.mtp-v2 wants an older version of it, we use the later version that's consistent with the MTP extension packages --- Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index fd61faa..bf40550 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,6 +11,7 @@ + From 4a3a46fe6d0afb0ea7d80c11661de4c4306cbd41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:52:39 +0000 Subject: [PATCH 096/115] Update Microsoft Testing Platform to 2.4.1 (571) 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 bf40550..6f2124c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true true - 2.4.0 + 2.4.1 From d691bea984b2684004d775ea2ffe77c7d9049fd8 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 16 Sep 2026 11:44:45 -0600 Subject: [PATCH 097/115] Pin Microsoft.Testing.Platform This is so that if xunit.v3.mtp-v2 wants an older version of it, we use the later version that's consistent with the MTP extension packages --- Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index cc0261b..2cf2188 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,6 +13,7 @@ + From 6d1fa2256e4565ee4b6c0016ca1a3fa45a45e6e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:52:39 +0000 Subject: [PATCH 098/115] Update Microsoft Testing Platform to 2.4.1 (571) 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 2cf2188..7fb9012 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true true - 2.4.0 + 2.4.1 2.0.257 From 8a940a21814bdf2463b19c113088bda5ed15630e Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 16 Sep 2026 13:00:47 -0600 Subject: [PATCH 099/115] Downgrade `dotnet-symbol` to meet CFS quarantine requirements --- .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 ac2120a..4536667 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -38,7 +38,7 @@ "rollForward": false }, "dotnet-symbol": { - "version": "10.0.745401", + "version": "10.0.731102", "commands": [ "dotnet-symbol" ], From 0868d932a56eeec779aa2124156111749c407aad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 06:49:44 -0600 Subject: [PATCH 100/115] Update dependency docfx to v2.78.6 (#572) 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 ac2120a..b65a25e 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -24,7 +24,7 @@ "rollForward": false }, "docfx": { - "version": "2.78.5", + "version": "2.78.6", "commands": [ "docfx" ], From f27557cbeb247df8a06d81aa830c457717934e65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 07:18:42 -0600 Subject: [PATCH 101/115] Update dependency ubuntu to v26 (574) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbaf6be..77ba156 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ run-name: ${{ github.ref_name }} jobs: release: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: read # Required to download artifacts contents: write # Upload artifacts to Release From 7b5f5bfa226bc7c32aed79544eb54b2838668d4d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 07:18:58 -0600 Subject: [PATCH 102/115] Update dependency docfx to v2.80.1 (573) 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 b65a25e..aed56a8 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -24,7 +24,7 @@ "rollForward": false }, "docfx": { - "version": "2.78.6", + "version": "2.80.1", "commands": [ "docfx" ], From 8637b8889cb125bd944665743160b5cb17f75275 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 08:23:08 -0600 Subject: [PATCH 103/115] Update mcr.microsoft.com/dotnet/sdk:10.0.401 Docker digest to 35d4030 (575) 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 7fbd7f5..e06f26a 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.401@sha256:2fa828c68761b1b8c23d7662dc134421b9d3b59fe1425fdbc80804e390cdb24d +FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:35d40304542c8689331f8cab17c65926cdf48fe711e289321d71924b230a7d29 # 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 d59c238d5246caa4d87230d6835451a843007c5a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 22 Sep 2026 08:41:08 -0600 Subject: [PATCH 104/115] Avoid re-installing .NET SDK or runtimes --- tools/Install-DotNetSdk.ps1 | 200 +++++++++++++++++++++++++----------- 1 file changed, 141 insertions(+), 59 deletions(-) diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1 index 2621dda..f4fcdaa 100644 --- a/tools/Install-DotNetSdk.ps1 +++ b/tools/Install-DotNetSdk.ps1 @@ -198,6 +198,66 @@ Function Install-DotNet($Version, $Architecture, [ValidateSet('Sdk','Runtime','W } } +Function Test-DotNetRuntimeInstalled( + $DotNetRoot, + [ValidateSet('Microsoft.NETCore.App','Microsoft.WindowsDesktop.App','Microsoft.AspNetCore.App')] + [string]$Runtime, + [Version]$Version +) { + if (!$DotNetRoot) { + return $false + } + + $runtimeRoot = Join-Path $DotNetRoot "shared\$Runtime" + if (!(Test-Path -LiteralPath $runtimeRoot)) { + return $false + } + + foreach ($runtimeDirectory in Get-ChildItem -LiteralPath $runtimeRoot -Directory) { + $installedVersion = $null + if ([Version]::TryParse($runtimeDirectory.Name, [ref]$installedVersion) -and + $Version.Major -eq $installedVersion.Major -and + $Version.Minor -eq $installedVersion.Minor -and + (($Version.Build -eq -1) -or ($Version.Build -eq $installedVersion.Build)) -and + (($Version.Revision -eq -1) -or ($Version.Revision -eq $installedVersion.Revision))) { + return $true + } + } + + return $false +} + +Function Test-DotNetSdkInstalled($DotNetRoot, $Version, $Channel) { + if (!$DotNetRoot) { + return $false + } + + $sdkRoot = Join-Path $DotNetRoot 'sdk' + if (!(Test-Path -LiteralPath $sdkRoot)) { + return $false + } + + if ($Version) { + return Test-Path -LiteralPath (Join-Path $sdkRoot $Version) -PathType Container + } + + $channelVersion = $null + if (![Version]::TryParse($Channel, [ref]$channelVersion)) { + return $false + } + + foreach ($sdkDirectory in Get-ChildItem -LiteralPath $sdkRoot -Directory) { + $installedVersion = $null + if ([Version]::TryParse($sdkDirectory.Name, [ref]$installedVersion) -and + $channelVersion.Major -eq $installedVersion.Major -and + $channelVersion.Minor -eq $installedVersion.Minor) { + return $true + } + } + + return $false +} + $switches = @() $envVars = @{ # For locally installed dotnet, skip first time experience which takes a long time @@ -208,53 +268,67 @@ if ($InstallLocality -eq 'machine') { if ($IsMacOS -or $IsLinux) { $DotNetInstallDir = '/usr/share/dotnet' } else { + $DotNetInstallDir = Join-Path $(if ($env:ProgramW6432) { $env:ProgramW6432 } else { $env:ProgramFiles }) 'dotnet' + $DotNetX86InstallDir = Join-Path ${env:ProgramFiles(x86)} 'dotnet' $restartRequired = $false $sdks |% { if ($_.Version) { $version = $_.Version } else { $version = $_.Channel } - if ($PSCmdlet.ShouldProcess(".NET SDK $version ($arch)", "Install")) { + if (!(Test-DotNetSdkInstalled -DotNetRoot $DotNetInstallDir -Version $_.Version -Channel $_.Channel) -and + $PSCmdlet.ShouldProcess(".NET SDK $version ($arch)", "Install")) { Install-DotNet -Version $version -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } - if ($IncludeX86) { - Install-DotNet -Version $version -Architecture x86 - $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) - } + if ($IncludeX86 -and + !(Test-DotNetSdkInstalled -DotNetRoot $DotNetX86InstallDir -Version $_.Version -Channel $_.Channel) -and + $PSCmdlet.ShouldProcess(".NET SDK $version (x86)", "Install")) { + Install-DotNet -Version $version -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) } } - $runtimeVersions | Sort-Object | Get-Unique |% { - if ($PSCmdlet.ShouldProcess(".NET runtime $_", "Install")) { + $runtimeVersions | Sort-Object -Unique |% { + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.NETCore.App -Version $_) -and + $PSCmdlet.ShouldProcess(".NET runtime $_ ($arch)", "Install")) { Install-DotNet -Version $_ -sku Runtime -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } - if ($IncludeX86) { - Install-DotNet -Version $_ -sku Runtime -Architecture x86 - $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) - } + if ($IncludeX86 -and + !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.NETCore.App -Version $_) -and + $PSCmdlet.ShouldProcess(".NET runtime $_ (x86)", "Install")) { + Install-DotNet -Version $_ -sku Runtime -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) } } - $windowsDesktopRuntimeVersions | Sort-Object | Get-Unique |% { - if ($PSCmdlet.ShouldProcess(".NET Windows Desktop $_", "Install")) { + $windowsDesktopRuntimeVersions | Sort-Object -Unique |% { + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_) -and + $PSCmdlet.ShouldProcess(".NET Windows Desktop $_ ($arch)", "Install")) { Install-DotNet -Version $_ -sku WindowsDesktop -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } - if ($IncludeX86) { - Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86 - $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) - } + if ($IncludeX86 -and + !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_) -and + $PSCmdlet.ShouldProcess(".NET Windows Desktop $_ (x86)", "Install")) { + Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) } } - $aspnetRuntimeVersions | Sort-Object | Get-Unique |% { - if ($PSCmdlet.ShouldProcess("ASP.NET Core $_", "Install")) { + $aspnetRuntimeVersions | Sort-Object -Unique |% { + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.AspNetCore.App -Version $_) -and + $PSCmdlet.ShouldProcess("ASP.NET Core $_ ($arch)", "Install")) { Install-DotNet -Version $_ -sku AspNetCore -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } - if ($IncludeX86) { - Install-DotNet -Version $_ -sku AspNetCore -Architecture x86 - $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) - } + if ($IncludeX86 -and + !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.AspNetCore.App -Version $_) -and + $PSCmdlet.ShouldProcess("ASP.NET Core $_ (x86)", "Install")) { + Install-DotNet -Version $_ -sku AspNetCore -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) } } if ($restartRequired) { @@ -317,19 +391,21 @@ $global:LASTEXITCODE = 0 $sdks |% { if ($_.Version) { $parameters = '-Version', $_.Version } else { $parameters = '-Channel', $_.Channel } - if ($PSCmdlet.ShouldProcess(".NET SDK $_ ($arch)", "Install")) { - $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches" + if (!(Test-DotNetSdkInstalled -DotNetRoot $DotNetInstallDir -Version $_.Version -Channel $_.Channel)) { + if ($PSCmdlet.ShouldProcess(".NET SDK $_ ($arch)", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches" - if ($LASTEXITCODE -ne 0) { - Write-Error ".NET SDK installation failure: $LASTEXITCODE" - exit $LASTEXITCODE + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun" } - } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun" } - if ($IncludeX86) { + if ($IncludeX86 -and !(Test-DotNetSdkInstalled -DotNetRoot $DotNetX86InstallDir -Version $_.Version -Channel $_.Channel)) { if ($PSCmdlet.ShouldProcess(".NET x86 SDK $_", "Install")) { $anythingInstalled = $true Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture x86 -InstallDir $DotNetX86InstallDir $switches" @@ -347,19 +423,21 @@ $sdks |% { $dotnetRuntimeSwitches = $switches + '-Runtime','dotnet' $runtimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) { - $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches" + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.NETCore.App -Version $_)) { + if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches" - if ($LASTEXITCODE -ne 0) { - Write-Error ".NET SDK installation failure: $LASTEXITCODE" - exit $LASTEXITCODE + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun" } - } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun" } - if ($IncludeX86) { + if ($IncludeX86 -and !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.NETCore.App -Version $_)) { if ($PSCmdlet.ShouldProcess(".NET x86 runtime $_", "Install")) { $anythingInstalled = $true Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches" @@ -377,19 +455,21 @@ $runtimeVersions | Sort-Object -Unique |% { $windowsDesktopRuntimeSwitches = $switches + '-Runtime','windowsdesktop' $windowsDesktopRuntimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) { - $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches" + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_)) { + if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches" - if ($LASTEXITCODE -ne 0) { - Write-Error ".NET SDK installation failure: $LASTEXITCODE" - exit $LASTEXITCODE + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun" } - } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun" } - if ($IncludeX86) { + if ($IncludeX86 -and !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_)) { if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop x86 runtime $_", "Install")) { $anythingInstalled = $true Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches" @@ -407,19 +487,21 @@ $windowsDesktopRuntimeVersions | Sort-Object -Unique |% { $aspnetRuntimeSwitches = $switches + '-Runtime','aspnetcore' $aspnetRuntimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) { - $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches" + if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.AspNetCore.App -Version $_)) { + if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches" - if ($LASTEXITCODE -ne 0) { - Write-Error ".NET SDK installation failure: $LASTEXITCODE" - exit $LASTEXITCODE + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun" } - } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun" } - if ($IncludeX86) { + if ($IncludeX86 -and !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.AspNetCore.App -Version $_)) { if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core x86 runtime $_", "Install")) { $anythingInstalled = $true Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches" From 13d25ee93ad38e0f8c965a44f5c76545717e2d50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:00:59 -0600 Subject: [PATCH 105/115] Update dependency docfx to v2.81.0 (577) --- .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 aed56a8..160aded 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -24,7 +24,7 @@ "rollForward": false }, "docfx": { - "version": "2.80.1", + "version": "2.81.0", "commands": [ "docfx" ], From fc7087efd845885ae744a43faa382b07fd357352 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 28 Sep 2026 09:39:38 -0600 Subject: [PATCH 106/115] Revert "Downgrade `dotnet-symbol` to meet CFS quarantine requirements" This reverts commit 8a940a21814bdf2463b19c113088bda5ed15630e. --- .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 4536667..ac2120a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -38,7 +38,7 @@ "rollForward": false }, "dotnet-symbol": { - "version": "10.0.731102", + "version": "10.0.745401", "commands": [ "dotnet-symbol" ], From f87898530c63a5ff6f5126cdd13a4873d87cb50a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Mon, 28 Sep 2026 16:27:50 -0600 Subject: [PATCH 107/115] Update becheran/mlc action to v1.2.2 --- .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 a2e22c5..7fe5157 100644 --- a/.github/workflows/docs_validate.yml +++ b/.github/workflows/docs_validate.yml @@ -17,7 +17,7 @@ jobs: with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: ๐Ÿ”— Markup Link Checker (mlc) - uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 + uses: becheran/mlc@b0d0d353e04fcbcfd1d86716103d78a61e5b25dd # v1.2.2 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://aka.ms/onboardsupport,https://aka.ms/spot,https://msrc.microsoft.com/*,https://www.microsoft.com/msrc*,https://microsoft.com/msrc*,https://www.npmjs.com/package/*,https://get.dot.net/ - name: โš™ Install prerequisites From 95dd30b4ca796f74be3833140f136f496648a7c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:48:35 -0600 Subject: [PATCH 108/115] Update becheran/mlc action to v1.2.2 (578) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64a106f..e210b36 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,6 +76,6 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: ๐Ÿ”— Markup Link Checker (mlc) - uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 + uses: becheran/mlc@b0d0d353e04fcbcfd1d86716103d78a61e5b25dd # v1.2.2 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/ From d4778325fe9cd707986b9a9eb3565107c66225a2 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 30 Sep 2026 11:50:55 -0600 Subject: [PATCH 109/115] Make docs workflow recoverable via re-run or manual dispatch Re-running a failed docs deployment failed because the prior attempt's github-pages artifact remained, making deploy-pages find two artifacts. Use a per-attempt artifact name, and allow workflow_dispatch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/docs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c0492a9..13dfd6d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,7 @@ on: push: branches: - main + workflow_dispatch: # 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. @@ -37,7 +38,12 @@ jobs: uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: docfx/_site + # Artifacts from prior attempts remain visible on re-runs, so make the name unique per attempt + # to avoid deploy-pages failing due to multiple artifacts with the same name. + name: github-pages-${{ github.run_attempt }} - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 + with: + artifact_name: github-pages-${{ github.run_attempt }} From 57fc2b2ee08529340f110ddb5d4ef98a97a8f167 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 10:29:38 -0600 Subject: [PATCH 110/115] Pin Ubuntu workflows to 26.04 Co-authored-by: Copilot <223556219+Copilot@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 +- azure-pipelines/build.yml | 2 +- azure-pipelines/libtemplate-update.yml | 2 +- azure-pipelines/release.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e210b36..db4b2e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: fail-fast: false matrix: os: - - ubuntu-24.04 + - ubuntu-26.04 - macOS-15 - windows-2025 @@ -72,7 +72,7 @@ jobs: docs: name: ๐Ÿ“ƒ Docs - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: ๐Ÿ”— Markup Link Checker (mlc) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 71adf9e..74a6256 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -16,7 +16,7 @@ on: jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. copilot-setup-steps: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 # Set the permissions to the lowest permissions possible needed for your steps. # Copilot will be given its own token for its operations. permissions: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 13dfd6d..9616f69 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/libtemplate-update.yml b/.github/workflows/libtemplate-update.yml index af1a9ee..3de947a 100644 --- a/.github/workflows/libtemplate-update.yml +++ b/.github/workflows/libtemplate-update.yml @@ -12,7 +12,7 @@ on: jobs: merge: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 permissions: contents: write pull-requests: write diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 7d808ed..3f15402 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -28,7 +28,7 @@ jobs: - job: Linux pool: - vmImage: ubuntu-24.04 + vmImage: ubuntu-26.04 steps: - checkout: self fetchDepth: 0 # avoid shallow clone so nbgv can do its work. diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml index fdfa1df..84ab4ca 100644 --- a/azure-pipelines/libtemplate-update.yml +++ b/azure-pipelines/libtemplate-update.yml @@ -22,7 +22,7 @@ stages: jobs: - job: merge pool: - vmImage: ubuntu-latest + vmImage: ubuntu-26.04 steps: - checkout: self fetchDepth: 0 diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index 4c9db86..a737d03 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -15,7 +15,7 @@ variables: jobs: - job: release pool: - vmImage: ubuntu-latest + vmImage: ubuntu-26.04 steps: - checkout: none - powershell: | From 3f29bf73558e39312648235bd19a8f5e38b870f3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 29 Sep 2026 20:58:54 -0600 Subject: [PATCH 111/115] Migrate from xUnit to TUnit --- .../template-release-notes.md | 24 +++++++++++ .github/renovate.json | 5 +++ .github/workflows/build.yml | 4 +- AGENTS.md | 32 ++++++--------- CONTRIBUTING.md | 16 ++++++++ Directory.Build.props | 17 ++++++++ Directory.Packages.props | 4 +- azure-pipelines/dotnet.yml | 8 +++- global.json | 3 ++ init.ps1 | 3 +- src/Directory.Build.props | 1 + src/dirs.proj | 5 +++ test/AOT.props | 38 +++++++++++++++++ test/Directory.Build.props | 7 ++++ test/Directory.Build.targets | 41 +++++++++++++++++-- test/Library.Tests/CalculatorTests.cs | 7 +--- test/Library.Tests/Library.Tests.csproj | 6 +-- test/Library.Tests/xunit.runner.json | 4 -- test/dirs.proj | 21 ++++++++++ tools/Get-NativeAOTTestProjects.ps1 | 25 +++++++++++ tools/dirs.proj | 6 +++ tools/dotnet-test-cloud.ps1 | 34 ++++++++++++++- 22 files changed, 268 insertions(+), 43 deletions(-) create mode 100644 src/dirs.proj create mode 100644 test/AOT.props delete mode 100644 test/Library.Tests/xunit.runner.json create mode 100644 test/dirs.proj create mode 100644 tools/Get-NativeAOTTestProjects.ps1 create mode 100644 tools/dirs.proj diff --git a/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md index 975da31..8bd6205 100644 --- a/.agents/skills/update-library-template/template-release-notes.md +++ b/.agents/skills/update-library-template/template-release-notes.md @@ -18,3 +18,27 @@ dotnet solution EXISTING.sln migrate This will create an EXISTING.slnx file. `git add` that file, then `git rm` the old `.sln` file. Sometimes a repo will reference the sln filename in a script or doc somewhere. Search the repo for such references and update them to the slnx file. + +## Migrating from xUnit to TUnit + +The template now uses TUnit with Microsoft.Testing.Platform instead of the xUnit test runner. +When merging this update into an existing repository, migrate all test projects and repo-specific test automation. +Follow the [TUnit xUnit migration guide](https://tunit.dev/docs/migration/xunit), including its automated migration code fixes. + +* Replace xUnit runner/framework package references with `TUnit.Engine` and set `Exe` in each test project. + Remove obsolete runner dependencies such as `xunit.runner.visualstudio`, `Microsoft.NET.Test.Sdk`, and `coverlet.collector`, along with `xunit.runner.json`. + Reconcile central package versions with the template's `Directory.Packages.props`. +* Existing xUnit assertions can remain: the template retains `xunit.v3.assert`, using `xunit.v3.assert.aot` for .NET 9 and later. + Keep `Xunit` imports for assertions, but use TUnit for test attributes and lifecycle APIs. +* Convert `[Fact]` and `[Theory]` to `[Test]`, and `[InlineData]` to `[Arguments]`. + Migrate member/class data sources, traits, fixtures, collection behavior, setup/teardown, and test output according to the migration guide. + Review tests that depend on xUnit's execution ordering or parallelization rules; do not assume those rules carry over. +* Merge the `global.json` Microsoft.Testing.Platform runner setting, test directory build files, traversal projects, and updated PowerShell/YAML automation. + Keep every test project in the repository's solution for managed test runs. + The traversals discover `.csproj` files under `src` and `test`; adjust discovery if your projects live elsewhere or use another project language. +* Update custom test commands and filters. + VSTest `--filter` expressions do not work with this runner; pass TUnit filters after `--`, for example `-- --treenode-filter "/*/*/ClassName/MethodName"`. + Replace any xUnit-specific CI result or coverage handling with the template's Microsoft.Testing.Platform integration. +* Validate managed tests and coverage, then run `dotnet publish tools\dirs.proj -c Release` and `.\tools\dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`. + Test projects targeting .NET 8 or later are eligible for NativeAOT publication by default. + If a project cannot support NativeAOT, set `false` in its project file; it will still run as managed tests. diff --git a/.github/renovate.json b/.github/renovate.json index 79dba1e..786187d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -11,6 +11,11 @@ "matchPackageNames": ["xunit*"], "groupName": "xunit" }, + { + "matchPackageNames": ["tunit*"], + "groupName": "tunit", + "schedule": ["on the first day of the month"] + }, { "matchPackageNames": ["Microsoft.Testing.Extensions.*"], "groupName": "Microsoft Testing Platform" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db4b2e0..bfe01ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,9 +45,9 @@ jobs: 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" + run: dotnet build tools/dirs.proj -t:build,pack,publish --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 }} + run: tools/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }} -IncludeNativeAOT shell: pwsh - name: ๐Ÿ’…๐Ÿป Verify formatted code run: dotnet format --verify-no-changes --no-restore diff --git a/AGENTS.md b/AGENTS.md index 0187843..49f4719 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,11 +13,10 @@ ## Testing -**IMPORTANT**: This repository uses Microsoft.Testing.Platform (MTP v2) with xunit v3. Traditional `--filter` syntax does NOT work. Use the options below instead. +**IMPORTANT**: This repository uses TUnit with Microsoft.Testing.Platform (MTP v2). Traditional `--filter` syntax does NOT work. Use the options below instead. * There should generally be one test project (under the `test` directory) per shipping project (under the `src` directory). Test projects are named after the project being tested with a `.Tests` suffix. -* Tests use xunit v3 with Microsoft.Testing.Platform (MTP v2). Traditional VSTest `--filter` syntax does NOT work. -* Some tests are known to be unstable. When running tests, you should skip the unstable ones by using `-- --filter-not-trait "FailsInCloudTest=true"`. +* Tests use TUnit with Microsoft.Testing.Platform (MTP v2), while retaining xUnit assertions. Traditional VSTest `--filter` syntax does NOT work. ### Running Tests @@ -33,47 +32,40 @@ dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Rele **Run a single test method**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-method ClassName.MethodName +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/ClassName/MethodName" ``` **Run all tests in a test class**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-class ClassName +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/ClassName/*" ``` **Run tests with wildcard matching** (supports wildcards at beginning and/or end): ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-method "*Pattern*" +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/*/*Pattern*" ``` -**Run tests with a specific trait** (equivalent to category filtering): +**Run tests with a specific property**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-trait "TraitName=value" -``` - -**Exclude tests with a specific trait** (skip unstable tests): -```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-not-trait "TestCategory=FailsInCloudTest" +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --treenode-filter "/*/*/*/*[PropertyName=value]" ``` **Run tests for a specific framework only**: ```bash -dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release --framework net9.0 +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release --framework net8.0 ``` **List all available tests without running them**: ```bash cd test/Library.Tests -dotnet run --no-build -c Release --framework net9.0 -- --list-tests +dotnet run --no-build -c Release --framework net8.0 -- --list-tests ``` -**Key points about test filtering with MTP v2 / xunit v3**: +**Key points about test filtering with TUnit / MTP v2**: - Options after `--` are passed to the test runner, not to `dotnet test` -- Use `--filter-method`, `--filter-class`, `--filter-namespace` for simple filtering -- Use `--filter-trait` and `--filter-not-trait` for trait-based filtering (replaces `--filter "TestCategory=..."`) +- Use `--treenode-filter` to select tests by assembly, namespace, class, method, or property - Traditional VSTest `--filter` expressions do NOT work -- Wildcards `*` are supported at the beginning and/or end of filter values -- Multiple simple filters of the same type use OR logic, different types combine with AND +- Wildcards `*` are supported in tree node segments - See `--help` for query filter language for advanced scenarios ## Coding style diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c566c0..bf43aad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,22 @@ 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`. +To build both managed and NativeAOT tests, run `dotnet publish tools/dirs.proj -c Release`. +Then run `./tools/dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`. +The traversal projects discover projects under `src` and `test`, and publish each eligible test framework targeting .NET 8 or later. +Keep test projects in the solution as well: managed test runs still use the solution, while NativeAOT runs use the traversal's evaluated executable paths. +Test projects can opt out of NativeAOT publishing with `false`. +One restore includes all test target frameworks, runtime identifiers, and NativeAOT compiler dependencies. +Managed builds are RID-neutral by default; NativeAOT builds use the SDK's runtime-specific output directories. +For a specified RID, managed execution and native publishing can share the same build outputs. +Test builds keep dynamic code, startup hooks, and event tracing enabled for managed code coverage. +The `ConfigureNativeAOTTestFeatures` target disables those features only in the native compiler's publish-time inputs, without rewriting the managed runtime configuration. +It preserves all other runtime feature options, including invariant globalization, so native compilation and linking use consistent settings. +For an existing RID-specific build, `dotnet publish test/Library.Tests/Library.Tests.csproj -f net8.0 -r -p:NativeAOT=true --no-build` publishes native tests from the managed build. +Use the same configuration, framework, and RID for the preceding build and the publish. +Test builds use invariant globalization and retain only English satellite resources; RID-specific builds are self-contained. +Root `Directory.Build.props` supplies project-reference defaults for both traversal and SDK projects that remove the `_IsPublishing` global property for managed dependencies, avoiding duplicate project instances that write to the same outputs during parallel publishing. + ## Releases Use `nbgv tag` to create a tag for a particular commit that you mean to release. diff --git a/Directory.Build.props b/Directory.Build.props index a3d5fb8..1a89035 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -48,11 +48,28 @@ 16.9 + + win + linux + osx + + x64 + arm64 + + $(RidOsPrefix)-$(RidOsArchitecture) + $(DefaultRuntimeIdentifier) + + + + + _IsPublishing + _IsPublishing + diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f2124c..11e3843 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,9 @@ - + + + diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index 9a572af..d399b31 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -3,10 +3,14 @@ parameters: steps: -- script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" +- script: dotnet build tools/dirs.proj -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" displayName: ๐Ÿ›  dotnet build -- powershell: tools/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults +- script: dotnet publish tools/dirs.proj --no-restore -c $(BuildConfiguration) + displayName: ๐Ÿ›  publish NativeAOT tests + condition: and(succeeded(), ${{ parameters.RunTests }}) + +- powershell: tools/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults -IncludeNativeAOT displayName: ๐Ÿงช dotnet test condition: and(succeeded(), ${{ parameters.RunTests }}) diff --git a/global.json b/global.json index 8c833e4..b925aab 100644 --- a/global.json +++ b/global.json @@ -6,5 +6,8 @@ }, "test": { "runner": "Microsoft.Testing.Platform" + }, + "msbuild-sdks": { + "Microsoft.Build.Traversal": "4.1.82" } } diff --git a/init.ps1 b/init.ps1 index b4004e7..3469e0b 100755 --- a/init.ps1 +++ b/init.ps1 @@ -90,10 +90,11 @@ try { if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) { Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor - dotnet restore @RestoreArguments + dotnet restore "$PSScriptRoot/tools/dirs.proj" @RestoreArguments if ($lastexitcode -ne 0) { throw "Failure while restoring packages." } + } if (!$NoToolRestore -and $PSCmdlet.ShouldProcess("dotnet tool", "restore")) { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9ba7818..b01ac12 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -9,4 +9,5 @@ + diff --git a/src/dirs.proj b/src/dirs.proj new file mode 100644 index 0000000..b6fa81f --- /dev/null +++ b/src/dirs.proj @@ -0,0 +1,5 @@ + + + + + diff --git a/test/AOT.props b/test/AOT.props new file mode 100644 index 0000000..9a252b6 --- /dev/null +++ b/test/AOT.props @@ -0,0 +1,38 @@ + + + true + $(AvailableRuntimeIdentifiers) + + + + true + true + true + true + en + + + true + + + + %(ProjectReference.GlobalPropertiesToRemove);RuntimeIdentifier;NativeAOT + + + + + + false + false + false + + + + + false + + + + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 2b5b542..035e949 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -3,9 +3,16 @@ + true + true false true true + false + + + + $(DefaultRuntimeIdentifier) diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index 3758bb8..95b0658 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -1,14 +1,47 @@ + + + + $(NoWarn);IL2104;IL3053 + + - - - - + + + + <_TestFramework Include="$(TargetFrameworks)" /> + + + + + + + + + + + + <_NativeExecutableExtension Condition="$([MSBuild]::IsOsPlatform('Windows'))">.exe + + + <_NativeAOTTestExecutable Include="$(MSBuildProjectFullPath)"> + $(MSBuildProjectName) + $(TargetFramework) + $([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(PublishDir)', '$(AssemblyName)$(_NativeExecutableExtension)')) + + + diff --git a/test/Library.Tests/CalculatorTests.cs b/test/Library.Tests/CalculatorTests.cs index a35947b..fb17881 100644 --- a/test/Library.Tests/CalculatorTests.cs +++ b/test/Library.Tests/CalculatorTests.cs @@ -2,15 +2,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Library; +using TUnit.Core; using Xunit; public class CalculatorTests { - public CalculatorTests() - { - } - - [Fact] + [Test] public void AddOrSubtract() { // This tests aggregation of code coverage across test runs. diff --git a/test/Library.Tests/Library.Tests.csproj b/test/Library.Tests/Library.Tests.csproj index e9e12e4..29e319f 100644 --- a/test/Library.Tests/Library.Tests.csproj +++ b/test/Library.Tests/Library.Tests.csproj @@ -1,18 +1,18 @@ - net8.0 $(TargetFrameworks);net472 Exe - - + + + diff --git a/test/Library.Tests/xunit.runner.json b/test/Library.Tests/xunit.runner.json deleted file mode 100644 index 8465a45..0000000 --- a/test/Library.Tests/xunit.runner.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "shadowCopy": false -} diff --git a/test/dirs.proj b/test/dirs.proj new file mode 100644 index 0000000..835c4a8 --- /dev/null +++ b/test/dirs.proj @@ -0,0 +1,21 @@ + + + + + + + + + + + + <_NativeAOTTest Update="@(_NativeAOTTest)" UndefineProperties="" /> + + + + + + + diff --git a/tools/Get-NativeAOTTestProjects.ps1 b/tools/Get-NativeAOTTestProjects.ps1 new file mode 100644 index 0000000..27a39ec --- /dev/null +++ b/tools/Get-NativeAOTTestProjects.ps1 @@ -0,0 +1,25 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Gets NativeAOT test executable paths evaluated by the test traversal project. +.PARAMETER Configuration + The build configuration used to evaluate output paths. +#> +[CmdletBinding()] +Param( + [string]$Configuration='Debug' +) + +$result = & dotnet msbuild "$PSScriptRoot/../test/dirs.proj" ` + -nologo ` + -getTargetResult:GetNativeAOTTests ` + "-property:Configuration=$Configuration" +if ($LASTEXITCODE -ne 0) { + throw 'Failed to discover NativeAOT tests through MSBuild.' +} + +$tests = ($result | Out-String | ConvertFrom-Json).TargetResults.GetNativeAOTTests.Items +foreach ($test in $tests) { + $test +} diff --git a/tools/dirs.proj b/tools/dirs.proj new file mode 100644 index 0000000..7b266e2 --- /dev/null +++ b/tools/dirs.proj @@ -0,0 +1,6 @@ + + + + + + diff --git a/tools/dotnet-test-cloud.ps1 b/tools/dotnet-test-cloud.ps1 index 3d8d087..1a00076 100644 --- a/tools/dotnet-test-cloud.ps1 +++ b/tools/dotnet-test-cloud.ps1 @@ -5,6 +5,8 @@ Runs tests as they are run in cloud test runs. .PARAMETER Configuration The configuration within which to run tests +.PARAMETER IncludeNativeAOT + Runs the NativeAOT-compiled tests and fails if the expected image is missing. .PARAMETER Agent The name of the agent. This is used in preparing test run titles. .PARAMETER PublishResults @@ -19,6 +21,7 @@ [CmdletBinding()] Param( [string]$Configuration='Debug', + [switch]$IncludeNativeAOT, [string]$Agent='Local', [switch]$PublishResults, [switch]$x86, @@ -50,6 +53,9 @@ if ($x86) { $testBinLog = Join-Path $ArtifactStagingFolder (Join-Path build_logs test.binlog) $testLogs = Join-Path $ArtifactStagingFolder test_logs +if (Test-Path -LiteralPath $testLogs) { + Remove-Item -LiteralPath $testLogs -Recurse -Force +} $globalJson = Get-Content $PSScriptRoot/../global.json | ConvertFrom-Json $isMTP = $globalJson.test.runner -eq 'Microsoft.Testing.Platform' @@ -95,12 +101,38 @@ if ($isMTP) { -c $Configuration ` -bl:"$testBinLog" ` -- ` - --filter-not-trait 'TestCategory=FailsInCloudTest' ` @mtpArgs ` @dumpSwitches ` @extraArgs if ($LASTEXITCODE -ne 0) { $failedTests += 1 } + if ($IncludeNativeAOT) { + $nativeAotTests = @(& "$PSScriptRoot/Get-NativeAOTTestProjects.ps1" -Configuration $Configuration) + foreach ($nativeAotTest in $nativeAotTests) { + $testExecutable = $nativeAotTest.ExecutablePath + if (-not (Test-Path -LiteralPath $testExecutable -PathType Leaf)) { + Write-Error "Expected NativeAOT TUnit test executable '$testExecutable' was not found." + $failedTests += 1 + continue + } + + $nativeAotArgs = @( + ,'--diagnostic' + ,'--diagnostic-output-directory',$testLogs + ,'--diagnostic-verbosity','Information' + ,'--results-directory',$testLogs + ,'--report-trx' + ,'--report-trx-filename',"$($nativeAotTest.ProjectName)_$($nativeAotTest.TargetFramework)_NativeAOT_{arch}.trx" + ) + if ($IsWindows) { + $nativeAotArgs += $dumpSwitches + } + Write-Host "Running NativeAOT TUnit tests from '$testExecutable'." -ForegroundColor Cyan + & $testExecutable @nativeAotArgs @extraArgs + if ($LASTEXITCODE -ne 0) { $failedTests += 1 } + } + } + $trxFiles = Get-ChildItem -Recurse -Path $testLogs\*.trx } else { $testDiagLog = Join-Path $ArtifactStagingFolder (Join-Path test_logs diag.log) From 302ccbeab00342f34c7e0299e39f7b22df2f1c07 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 10:16:35 -0600 Subject: [PATCH 112/115] Build and test NativeAOT-safe libraries --- .../template-release-notes.md | 9 +++++++++ CONTRIBUTING.md | 3 +++ Expand-Template.ps1 | 6 ++++++ Library.slnx | 1 + README.md | 1 + src/Library/Library.csproj | 1 + test/AOT.props | 4 +++- .../AotCompatibilityTest.csproj | 19 +++++++++++++++++++ test/AotCompatibilityTest/Program.cs | 4 ++++ test/dirs.proj | 2 ++ 10 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 test/AotCompatibilityTest/AotCompatibilityTest.csproj create mode 100644 test/AotCompatibilityTest/Program.cs diff --git a/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md index 8bd6205..7ce3f9c 100644 --- a/.agents/skills/update-library-template/template-release-notes.md +++ b/.agents/skills/update-library-template/template-release-notes.md @@ -42,3 +42,12 @@ Follow the [TUnit xUnit migration guide](https://tunit.dev/docs/migration/xunit) * Validate managed tests and coverage, then run `dotnet publish tools\dirs.proj -c Release` and `.\tools\dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`. Test projects targeting .NET 8 or later are eligible for NativeAOT publication by default. If a project cannot support NativeAOT, set `false` in its project file; it will still run as managed tests. + +## NativeAOT compatibility validation + +Shipping projects now set `IsAotCompatible` for target frameworks compatible with .NET 8. +Apply the same conditional property to each shipping project that is intended to support NativeAOT, then resolve all trim and AOT analyzer warnings rather than suppressing them broadly. + +The new `test/AotCompatibilityTest` project catches NativeAOT compiler warnings that library analyzers may not report. +Keep it in the solution, add every NativeAOT-compatible shipping assembly as a `TrimmerRootAssembly`, and retain its publishable entry in `test/dirs.proj`. +CI validates this project through the existing traversal publish on Windows, Linux, and macOS; it is intentionally marked `IsTestProject=false` because publishing successfully is the test. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf43aad..5bda7fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,6 +59,9 @@ It preserves all other runtime feature options, including invariant globalizatio For an existing RID-specific build, `dotnet publish test/Library.Tests/Library.Tests.csproj -f net8.0 -r -p:NativeAOT=true --no-build` publishes native tests from the managed build. Use the same configuration, framework, and RID for the preceding build and the publish. Test builds use invariant globalization and retain only English satellite resources; RID-specific builds are self-contained. +Shipping libraries targeting .NET 8 or later opt into NativeAOT compatibility analysis with `IsAotCompatible`. +The `test/AotCompatibilityTest` project complements those analyzers by rooting the shipping assembly and passing it through the NativeAOT compiler during every traversal publish. +Add each shipping assembly that must be validated as a `TrimmerRootAssembly`, and keep this project publishable in `test/dirs.proj`. Root `Directory.Build.props` supplies project-reference defaults for both traversal and SDK projects that remove the `_IsPublishing` global property for managed dependencies, avoiding duplicate project instances that write to the same outputs during parallel publishing. ## Releases diff --git a/Expand-Template.ps1 b/Expand-Template.ps1 index eb52532..37e96b5 100755 --- a/Expand-Template.ps1 +++ b/Expand-Template.ps1 @@ -140,6 +140,12 @@ try { 'Library'=$LibraryName 'COMPANY-PLACEHOLDER'=$Author } + Replace-Placeholders -Path "test/AotCompatibilityTest/AotCompatibilityTest.csproj" -Replacements @{ + 'Library'=$LibraryName + } + Replace-Placeholders -Path "test/AotCompatibilityTest/Program.cs" -Replacements @{ + 'COMPANY-PLACEHOLDER'=$Author + } Replace-Placeholders -Path "src/AssemblyInfo.cs" -Replacements @{ 'COMPANY-PLACEHOLDER'=$Author } diff --git a/Library.slnx b/Library.slnx index dc016d9..4d7254f 100644 --- a/Library.slnx +++ b/Library.slnx @@ -22,5 +22,6 @@ + diff --git a/README.md b/README.md index 3f8c976..1f6ce98 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ * Auto-versioning (via [Nerdbank.GitVersioning](https://github.com/dotnet/nerdbank.gitversioning)) * Builds with a "pinned" .NET SDK to ensure reproducible builds across machines and across time. * Automatically pack the library and publish it as an artifact, and even push it to some NuGet feed for consumption. +* Build NativeAOT-safe libraries with thorough compatibility testing * Testing * Testing on .NET Framework, multiple .NET versions * Testing on Windows, Linux and OSX diff --git a/src/Library/Library.csproj b/src/Library/Library.csproj index fa79c05..e4cd98a 100644 --- a/src/Library/Library.csproj +++ b/src/Library/Library.csproj @@ -1,6 +1,7 @@ net8.0;netstandard2.0 + true diff --git a/test/AOT.props b/test/AOT.props index 9a252b6..623ff24 100644 --- a/test/AOT.props +++ b/test/AOT.props @@ -3,11 +3,13 @@ true $(AvailableRuntimeIdentifiers) - + true true true + + true en diff --git a/test/AotCompatibilityTest/AotCompatibilityTest.csproj b/test/AotCompatibilityTest/AotCompatibilityTest.csproj new file mode 100644 index 0000000..f8a3736 --- /dev/null +++ b/test/AotCompatibilityTest/AotCompatibilityTest.csproj @@ -0,0 +1,19 @@ + + + net10.0 + Exe + false + $(DefaultRuntimeIdentifier) + true + + + + + + + + + + + + diff --git a/test/AotCompatibilityTest/Program.cs b/test/AotCompatibilityTest/Program.cs new file mode 100644 index 0000000..1467eff --- /dev/null +++ b/test/AotCompatibilityTest/Program.cs @@ -0,0 +1,4 @@ +// Copyright (c) COMPANY-PLACEHOLDER. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +Console.WriteLine("This project validates NativeAOT compatibility when it is published; the executable is not run."); diff --git a/test/dirs.proj b/test/dirs.proj index 835c4a8..6f6def8 100644 --- a/test/dirs.proj +++ b/test/dirs.proj @@ -1,6 +1,8 @@ + + From a3c19c065e59510f19cec90f62cb75eef346a933 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 18:42:20 -0600 Subject: [PATCH 113/115] Add opt-out instructions for the NativeAOT support --- .../template-release-notes.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md index 7ce3f9c..1a731e6 100644 --- a/.agents/skills/update-library-template/template-release-notes.md +++ b/.agents/skills/update-library-template/template-release-notes.md @@ -51,3 +51,31 @@ Apply the same conditional property to each shipping project that is intended to The new `test/AotCompatibilityTest` project catches NativeAOT compiler warnings that library analyzers may not report. Keep it in the solution, add every NativeAOT-compatible shipping assembly as a `TrimmerRootAssembly`, and retain its publishable entry in `test/dirs.proj`. CI validates this project through the existing traversal publish on Windows, Linux, and macOS; it is intentionally marked `IsTestProject=false` because publishing successfully is the test. + +## NativeAOT opt-out and AI merge guidance + +Not every repo wants to take on NativeAOT readiness or NativeAOT-published test validation. +If a receiving repo does not intend to support NativeAOT, opt out explicitly instead of inheriting the template's defaults. + +For test projects that should remain managed-only: + +* Set `false` in the project file or in a repo-level `Directory.Build.props`. +* Leave the managed test project in the solution so usual IL-based testing continues to work. +* Keep `dotnet test` and the managed coverage flow unchanged; only the NativeAOT publish step is disabled. + +For shipping libraries that do not want NativeAOT compatibility analysis: + +* Remove `IsAotCompatible` from the project, or set it only for specific target frameworks that truly support NativeAOT. +* Do not add new AOT compatibility warnings to the build if the library is not intended to be AOT-compatible. +* If the repo does not target .NET 8 or later, the property should be omitted entirely rather than applied unconditionally. + +For the compatibility project itself: + +* Remove the `test/AotCompatibilityTest` project from the solution if NativeAOT validation is out of scope. +* Remove its entry from `test/dirs.proj` so the traversal publish no longer publishes it. +* Keep the project only when the repo intends to validate shipping assemblies under NativeAOT in CI. + +When merging this template into a downstream repo, the AI assistant should ask whether NativeAOT is a relevant target for that repo before applying these changes if `IsAotCompatible` is not already set by any of the projects. +If the answer is no, it should apply the opt-out steps above instead of blindly keeping the NativeAOT defaults. +This is especially important for repos that are not shipping a NativeAOT-ready library or that cannot support the extra publish-time validation in CI. +If the repo operator is unsure about NativeAOT support, you might offer to test the repo for NativeAOT readiness by attempting a NativeAOT publish of the relevant projects and observing whether any compatibility warnings or errors are produced. From 5e9fca2e2173de7753112ea8447df56f06f17d87 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 19:11:22 -0600 Subject: [PATCH 114/115] Improve release notes further --- .../skills/update-library-template/template-release-notes.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md index 1a731e6..0ef41c2 100644 --- a/.agents/skills/update-library-template/template-release-notes.md +++ b/.agents/skills/update-library-template/template-release-notes.md @@ -78,4 +78,7 @@ For the compatibility project itself: When merging this template into a downstream repo, the AI assistant should ask whether NativeAOT is a relevant target for that repo before applying these changes if `IsAotCompatible` is not already set by any of the projects. If the answer is no, it should apply the opt-out steps above instead of blindly keeping the NativeAOT defaults. This is especially important for repos that are not shipping a NativeAOT-ready library or that cannot support the extra publish-time validation in CI. -If the repo operator is unsure about NativeAOT support, you might offer to test the repo for NativeAOT readiness by attempting a NativeAOT publish of the relevant projects and observing whether any compatibility warnings or errors are produced. +If the repo owner is unsure about NativeAOT support, you might offer to test the repo for NativeAOT readiness by attempting a NativeAOT publish of the relevant projects and observing whether any compatibility warnings or errors are produced. + +In the event the user elects for NativeAOT targeting, make efforts to get tests to work under NativeAOT as well so the shipping code can confidently advertise its NativeAOT compatibility. +If aspects of the library or tests make successful builds, publish or test runs problematic, discuss this with the repo owner before making significant changes or giving up on NativeAOT support. From 3eebddd0f5a3f5cd314c97a2acbed43000cc29e3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 1 Oct 2026 19:16:07 -0600 Subject: [PATCH 115/115] Build and publish in one step in Azure Pipelines --- azure-pipelines/dotnet.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index d399b31..91568ab 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -3,12 +3,13 @@ parameters: steps: -- script: dotnet build tools/dirs.proj -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" - displayName: ๐Ÿ›  dotnet build +- ${{ if parameters.RunTests }}: + - script: dotnet build tools/dirs.proj -t:build,pack,publish --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" + displayName: ๐Ÿ›  dotnet build -- script: dotnet publish tools/dirs.proj --no-restore -c $(BuildConfiguration) - displayName: ๐Ÿ›  publish NativeAOT tests - condition: and(succeeded(), ${{ parameters.RunTests }}) +- ${{ if not(parameters.RunTests) }}: + - script: dotnet build tools/dirs.proj -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" + displayName: ๐Ÿ›  dotnet build - powershell: tools/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults -IncludeNativeAOT displayName: ๐Ÿงช dotnet test