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/101] 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 c19331d4..23e8a058 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 b9d46b7c..42d1dab7 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/101] 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 23e8a058..ba9b521f 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 42d1dab7..4319884e 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 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 003/101] 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 f88bae60..f82fc36a 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 004/101] 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 69e7ae4f..110b0af5 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 d933871a..e61bfd52 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 99b3c058..6f0fbaa5 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 fc8a631f..6336d638 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 005/101] 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 f82fc36a..2ddce27d 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 8630c458..82ce8d72 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 006/101] 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 110b0af5..94a48d76 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 e61bfd52..c0436e85 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 6f0fbaa5..9729dc34 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 6336d638..dfd3f578 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 007/101] 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 211881fa..9e075df5 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 384be7cf..4efbd207 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 22a5d6a3..38e95f2f 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 9f934f92..675912bc 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 edc56529..a16f98a4 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 36aaf6e5..51752535 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 6a839d47..0eb2c2ab 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 008/101] 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 08c01850..635ed8c6 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 009/101] 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 82ce8d72..fa4d2036 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 010/101] 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 fa4d2036..31eb8113 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 011/101] 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 9e075df5..cb8d68e9 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 4efbd207..acbfbace 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 38e95f2f..99815ebb 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 a16f98a4..8b7f8c90 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 51752535..efc60f32 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 5e5a476c..edb114c4 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 0eb2c2ab..3f2a194f 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 012/101] 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 48dedbe9..3fec6683 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 013/101] 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 c742a588..8678d0da 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 0816c0e2..c2d95b8f 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 014/101] 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 dfa9cba4..90fd080d 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 015/101] 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 8678d0da..1546d991 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 016/101] 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 675912bc..d7723325 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 8b7f8c90..dda671c0 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 017/101] 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 2ddce27d..9484456c 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 018/101] 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 31eb8113..2e095f23 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 019/101] 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 9484456c..2ddce27d 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 020/101] 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 1546d991..fbe54390 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 021/101] 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 fbe54390..a2a04c5d 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 022/101] 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 c2d95b8f..c85d6941 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 023/101] 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 d7723325..9abd3770 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 dda671c0..a8218f05 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 024/101] 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 90fd080d..ba081f0f 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 025/101] 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 a2a04c5d..c732a0f9 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 026/101] 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 9484456c..30cea5a7 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 2e095f23..ca1e17d7 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 027/101] 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 d9d35bb1..bec37c9e 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 00000000..bc0cba65 --- /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 00000000..e62264b1 --- /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 00000000..6180745e --- /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 028/101] 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 edb114c4..9f5743ae 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 3f2a194f..cbfeb30b 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 029/101] 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 ca1e17d7..11bef295 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 030/101] 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 cbfeb30b..e7e4c978 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 031/101] 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 2ddce27d..9484456c 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 032/101] 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 033/101] 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 11bef295..6280399d 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 034/101] 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 635ed8c6..f18d31b1 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 4c9fb87c..dba80e90 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 035/101] 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 94a48d76..3caef376 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 c0436e85..71adf9e7 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 9729dc34..c5a2fef4 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 dfd3f578..af1a9eef 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 036/101] 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 30cea5a7..56c4ec6e 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 037/101] 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 30cea5a7..ac392d05 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 038/101] 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 1a1af61e..56c4ec6e 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 039/101] 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 a841967e..b988010f 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 040/101] 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 3caef376..3e7e2f0b 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 c5a2fef4..c462a089 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 041/101] 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 6280399d..d026d2e8 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 042/101] 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 b988010f..1817ca09 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 043/101] 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 f18d31b1..6d2dbf45 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 044/101] 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 3e7e2f0b..64a106f5 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 734ee607..893a3534 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 1d736511..2f9b85a6 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 045/101] 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 1817ca09..d96c8847 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 5d204819..da876cb9 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 046/101] 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 6d2dbf45..cfa41d1f 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 dba80e90..d0cca713 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 047/101] 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 bec37c9e..2621dda1 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 048/101] 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 da876cb9..7ecaf89c 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 2f9b85a6..c393b7d9 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 049/101] 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 7ecaf89c..3d8d0870 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 c393b7d9..d9bc81db 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 050/101] 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 56c4ec6e..7b7805ba 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 051/101] 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 d026d2e8..462d1d04 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 052/101] 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 7b7805ba..56c4ec6e 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 053/101] 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 7b7805ba..636973a2 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 054/101] 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 462d1d04..7dcdc199 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 055/101] 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 636973a2..3abeb5a0 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 056/101] 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 cfa41d1f..566f9e48 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 057/101] 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 05e4d16b..dae8d956 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 058/101] Document the requirement for a repo secret --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dae8d956..2c566c0b 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 059/101] 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 7b133221..bbaf6be7 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 060/101] 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 7dcdc199..ffa3b43b 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 061/101] 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 ffa3b43b..6d844933 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 062/101] 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 cb8d68e9..13015bef 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 acbfbace..8b342de2 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 99815ebb..084d0d82 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 a8218f05..3df977e5 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 efc60f32..fd593797 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 9f5743ae..1b5313e6 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 e7e4c978..b4037eed 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 063/101] 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 c3d8ce3f..f9ef4a96 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 064/101] Drop GitHub Actions section --- CONTRIBUTING.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c566c0b..2af8f46f 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 065/101] 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 c6c30097..3abeb5a0 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 066/101] 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 3abeb5a0..097e92d1 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 6d844933..8ad421c8 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 067/101] 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 c462a089..c0492a90 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 068/101] 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 097e92d1..db2bb854 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 069/101] 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 8ad421c8..bdf5637b 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 070/101] 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 db2bb854..ad207b6f 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 071/101] 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 566f9e48..694e58bc 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 072/101] 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 ad207b6f..79a9fb76 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 073/101] 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 694e58bc..1b15368a 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 d0cca713..8c833e41 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 074/101] 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 1b15368a..c9af5a4b 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 075/101] 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 79a9fb76..9575f99e 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 076/101] 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 9575f99e..ac2120a2 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 077/101] 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 bdf5637b..2c5250e2 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 078/101] 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 c9af5a4b..7fbd7f50 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 079/101] 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 2c5250e2..fd61faae 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 080/101] 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 1b5313e6..3f8a4d8c 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 b4037eed..70e9c45e 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 081/101] 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 fd61faae..bf405505 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 082/101] 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 bf405505..6f2124c1 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 083/101] 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 cc0261bf..2cf21888 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 084/101] 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 2cf21888..7fb90124 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 085/101] 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 ac2120a2..45366674 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 086/101] 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 ac2120a2..b65a25e6 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 087/101] 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 bbaf6be7..77ba156d 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 088/101] 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 b65a25e6..aed56a8d 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 089/101] 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 7fbd7f50..e06f26ac 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 090/101] 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 2621dda1..f4fcdaae 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 091/101] 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 aed56a8d..160aded9 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 092/101] 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 45366674..ac2120a2 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 093/101] 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 a2e22c5a..7fe51570 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 094/101] 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 64a106f5..e210b36c 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 095/101] 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 c0492a90..13dfd6d5 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 096/101] 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 e210b36c..db4b2e0f 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 71adf9e7..74a62566 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 13dfd6d5..9616f694 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 af1a9eef..3de947a1 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 7d808ede..3f154027 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 fdfa1df5..84ab4cae 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 4c9db860..a737d039 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 097/101] 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 975da315..8bd62058 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 79dba1e5..786187da 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 db4b2e0f..bfe01efd 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 0187843a..49f47191 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 2c566c0b..bf43aad4 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 a3d5fb8e..1a890354 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 6f2124c1..11e38433 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 9a572afe..d399b31b 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 8c833e41..b925aaba 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 b4004e79..3469e0b7 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 9ba7818d..b01ac125 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 00000000..b6fa81f9 --- /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 00000000..9a252b65 --- /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 2b5b542d..035e949c 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 3758bb8c..95b0658b 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 a35947bc..fb178818 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 e9e12e43..29e319fd 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 8465a454..00000000 --- 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 00000000..835c4a89 --- /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 00000000..27a39ec3 --- /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 00000000..7b266e21 --- /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 3d8d0870..1a000768 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 098/101] 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 8bd62058..7ce3f9c7 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 bf43aad4..5bda7fc9 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 eb525325..37e96b57 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 dc016d9e..4d7254f1 100644 --- a/Library.slnx +++ b/Library.slnx @@ -22,5 +22,6 @@ + diff --git a/README.md b/README.md index 3f8c9764..1f6ce981 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 fa79c05e..e4cd98aa 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 9a252b65..623ff24d 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 00000000..f8a37360 --- /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 00000000..1467eff9 --- /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 835c4a89..6f6def8c 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 099/101] 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 7ce3f9c7..1a731e63 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 100/101] 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 1a731e63..0ef41c2c 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 101/101] 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 d399b31b..91568abb 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