From af9211b1364d382bc386cd82c6386875ad2ad796 Mon Sep 17 00:00:00 2001 From: mahabaleshwars <147705296+mahabaleshwars@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:54:18 +0530 Subject: [PATCH 1/5] Add dotnet-version: latest support with dotnet-channel input (#730) * feat: add dotnet-version: latest keyword with dotnet-channel support (#497) * restore test-proxy container image * update e2e-tests.yml and documentation * fix(tests): correct release-type and support-phase values in latest-version test mocks --- .github/workflows/e2e-tests.yml | 41 ++++++ README.md | 42 +++++- __tests__/installer.test.ts | 27 ++-- __tests__/latest-version.test.ts | 223 +++++++++++++++++++++++++++++++ __tests__/setup-dotnet.test.ts | 92 ++++++++++++- action.yml | 8 +- dist/setup/index.js | 128 ++++++++++++++++-- src/installer.ts | 124 ++++++++++++++++- src/setup-dotnet.ts | 52 +++++-- 9 files changed, 687 insertions(+), 50 deletions(-) create mode 100644 __tests__/latest-version.test.ts diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d670813..4bda39d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -649,3 +649,44 @@ jobs: - name: Verify dotnet shell: pwsh run: __tests__/verify-dotnet.ps1 -Patterns "^8.0.416$", "^9.0.308$", "^10.0.101$", "^8.0" + + test-setup-latest-version: + runs-on: ${{ matrix.operating-system }} + strategy: + fail-fast: false + matrix: + operating-system: [ubuntu-latest] + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Clear toolcache + shell: pwsh + run: __tests__/clear-toolcache.ps1 ${{ runner.os }} + - name: Setup dotnet latest + uses: ./ + with: + dotnet-version: latest + - name: Verify dotnet + shell: pwsh + run: __tests__/verify-dotnet.ps1 -Patterns "^\d+\.\d+\.\d+" + + test-setup-latest-with-channel-abcxx: + runs-on: ${{ matrix.operating-system }} + strategy: + fail-fast: false + matrix: + operating-system: [macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Clear toolcache + shell: pwsh + run: __tests__/clear-toolcache.ps1 ${{ runner.os }} + - name: Setup dotnet latest with A.B.Cxx channel + uses: ./ + with: + dotnet-version: latest + dotnet-channel: '9.0.1xx' + - name: Verify dotnet + shell: pwsh + run: __tests__/verify-dotnet.ps1 -Patterns "^9\.0\.1\d{2}" diff --git a/README.md b/README.md index 4465ee7..ee6b181 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,30 @@ The `dotnet-version` input supports following syntax: - **A.B** or **A.B.x** (e.g. 8.0, 8.0.x) - installs the latest patch version of .NET SDK on the channel `8.0`, including prerelease versions (preview, rc) - **A** or **A.x** (e.g. 8, 8.x) - installs the latest minor version of the specified major tag, including prerelease versions (preview, rc) - **A.B.Cxx** (e.g. 8.0.4xx) - available since `.NET 5.0` release. Installs the latest version of the specific SDK release, including prerelease versions (preview, rc). +- **latest** - dynamically resolves to the highest active .NET SDK version. By default, it installs the latest **stable (GA)** version (excluding previews and end-of-life releases). Can be combined with `dotnet-channel` and `dotnet-quality`. +## Using with `dotnet-channel` input + +The optional `dotnet-channel` input specifies the source channel for the installation. Supported values: + +| Value | Description | +|-------|-------------| +| `STS` | The most recent Standard Term Support release | +| `LTS` | The most recent Long Term Support release | +| `A.B` (e.g. `8.0`) | A specific release channel | +| `A.B.Cxx` (e.g. `8.0.1xx`) | A specific SDK release (available since 5.0) | + +> **Note**: The `dotnet-channel` input is only applied when `dotnet-version` is set to `latest`. If used with a specific version, a warning will be logged and the channel input will be ignored. + +**Install latest LTS version:** +```yaml +steps: +- uses: actions/checkout@v6 +- uses: actions/setup-dotnet@v5 + with: + dotnet-version: latest + dotnet-channel: LTS +``` ## Using the `architecture` input Using the architecture input, it is possible to specify the required .NET SDK architecture. Possible values: `x64`, `x86`, `arm64`, `amd64`, `arm`, `s390x`, `ppc64le`, `riscv64`. If the input is not specified, the architecture defaults to the host OS architecture (not all of the architectures are available on all platforms). @@ -77,9 +100,10 @@ steps: ``` ## Using the `dotnet-quality` input -This input sets up the action to install the latest build of the specified quality in the channel. The possible values of `dotnet-quality` are: **daily**, **signed**, **validated**, **preview**, **ga**. -> **Note**: `dotnet-quality` input can be used only with .NET SDK version in 'A.B', 'A.B.x', 'A', 'A.x' and 'A.B.Cxx' formats where the major version is higher than 5. In other cases, `dotnet-quality` input will be ignored. +The `dotnet-quality` input installs the latest build of the specified quality in the channel. Supported values: `daily`, `preview`, `ga`. + +> **Note**: When used with a specific SDK version, `dotnet-quality` supports only `A.B`, `A.B.x`, `A`, `A.x`, and `A.B.Cxx` formats where the major version is higher than 5. For all other formats, `dotnet-quality` will be ignored. ```yml steps: @@ -91,6 +115,18 @@ steps: - run: dotnet build ``` +`dotnet-quality` can also be combined with `dotnet-version: latest` and `dotnet-channel` to target specific builds such as the latest `daily` build from the `LTS` channel. + +```yaml +steps: +- uses: actions/checkout@v6 +- uses: actions/setup-dotnet@v5 + with: + dotnet-version: latest + dotnet-channel: LTS + dotnet-quality: daily +``` + ## Using the `global-json-file` input `setup-dotnet` action can read .NET SDK version from a `global.json` file. Input `global-json-file` is used for specifying the path to the `global.json`. If the file that was supplied to `global-json-file` input doesn't exist, the action will fail with error. @@ -371,4 +407,4 @@ The scripts and documentation in this project are released under the [MIT Licens ## Contributions -Contributions are welcome! See [Contributor's Guide](docs/contributors.md) +Contributions are welcome! See [Contributor's Guide](docs/contributors.md) \ No newline at end of file diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 11a57be..851c327 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -9,7 +9,6 @@ import * as io from '@actions/io'; import * as installer from '../src/installer'; import {IS_WINDOWS} from '../src/utils'; -import {QualityOptions} from '../src/setup-dotnet'; describe('installer tests', () => { const env = process.env; @@ -40,7 +39,7 @@ describe('installer tests', () => { it('should throw the error in case of non-zero exit code of the installation script. The error message should contain logs.', async () => { const inputVersion = '10.0.101'; - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const errorMessage = 'fictitious error message!'; getExecOutputSpy.mockImplementation(() => { @@ -62,7 +61,7 @@ describe('installer tests', () => { it('should return version of .NET SDK after installation complete', async () => { const inputVersion = '10.0.101'; - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { return Promise.resolve({ @@ -84,7 +83,7 @@ describe('installer tests', () => { it(`should supply 'version' argument to the installation script if supplied version is in A.B.C syntax`, async () => { const inputVersion = '10.0.101'; - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { @@ -122,7 +121,7 @@ describe('installer tests', () => { it(`should warn if the 'quality' input is set and the supplied version is in A.B.C syntax`, async () => { const inputVersion = '10.0.101'; - const inputQuality = 'ga' as QualityOptions; + const inputQuality = 'ga'; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { return Promise.resolve({ @@ -147,7 +146,7 @@ describe('installer tests', () => { it(`should warn if the 'quality' input is set and version isn't in A.B.C syntax but major tag is lower then 6`, async () => { const inputVersion = '3.1'; - const inputQuality = 'ga' as QualityOptions; + const inputQuality = 'ga'; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { @@ -174,7 +173,7 @@ describe('installer tests', () => { each(['10', '10.0', '10.0.x', '10.0.*', '10.0.X']).test( `should supply 'quality' argument to the installation script if quality input is set and version (%s) is not in A.B.C syntax`, async inputVersion => { - const inputQuality = 'ga' as QualityOptions; + const inputQuality = 'ga'; const exitCode = 0; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { @@ -214,7 +213,7 @@ describe('installer tests', () => { each(['10', '10.0', '10.0.x', '10.0.*', '10.0.X']).test( `should supply 'channel' argument to the installation script if version (%s) isn't in A.B.C syntax`, async inputVersion => { - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const exitCode = 0; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { @@ -255,7 +254,7 @@ describe('installer tests', () => { it(`should supply '-ProxyAddress' argument to the installation script if env.variable 'https_proxy' is set`, async () => { process.env['https_proxy'] = 'https://proxy.com'; const inputVersion = '10.0.101'; - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { @@ -293,7 +292,7 @@ describe('installer tests', () => { it(`should supply '-ProxyBypassList' argument to the installation script if env.variable 'no_proxy' is set`, async () => { process.env['no_proxy'] = 'first.url,second.url'; const inputVersion = '10.0.101'; - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { @@ -331,7 +330,7 @@ describe('installer tests', () => { it(`should supply 'architecture' argument to the installation script when architecture is provided`, async () => { const inputVersion = '10.0.101'; - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const inputArchitecture = 'x64'; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; @@ -365,7 +364,7 @@ describe('installer tests', () => { it(`should NOT supply 'architecture' argument when architecture is not provided`, async () => { const inputVersion = '10.0.101'; - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { @@ -395,7 +394,7 @@ describe('installer tests', () => { it(`should supply 'install-dir' with arch subdirectory for cross-arch install`, async () => { const inputVersion = '10.0.101'; - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const inputArchitecture = 'x64'; const stdout = `Fictitious dotnet version ${inputVersion} is installed`; @@ -436,7 +435,7 @@ describe('installer tests', () => { it(`should NOT supply 'install-dir' when architecture matches runner's native arch`, async () => { const inputVersion = '10.0.101'; - const inputQuality = '' as QualityOptions; + const inputQuality = ''; const nativeArch = os.arch().toLowerCase(); const stdout = `Fictitious dotnet version ${inputVersion} is installed`; diff --git a/__tests__/latest-version.test.ts b/__tests__/latest-version.test.ts new file mode 100644 index 0000000..e5e637a --- /dev/null +++ b/__tests__/latest-version.test.ts @@ -0,0 +1,223 @@ +import {DotnetVersionResolver} from '../src/installer'; +import * as hc from '@actions/http-client'; +import * as core from '@actions/core'; + +// Mock http-client +jest.mock('@actions/http-client'); + +describe('DotnetVersionResolver with latest', () => { + let getJsonMock: jest.Mock; + let warningSpy: jest.SpyInstance; + + beforeEach(() => { + getJsonMock = jest.fn(); + (hc.HttpClient as any).mockImplementation(() => { + return { + getJson: getJsonMock + }; + }); + warningSpy = jest.spyOn(core, 'warning').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const mockReleases = { + 'releases-index': [ + { + 'channel-version': '10.0', + 'support-phase': 'preview', + 'release-type': 'lts' + }, + { + 'channel-version': '9.0', + 'support-phase': 'active', + 'release-type': 'sts' + }, + { + 'channel-version': '8.0', + 'support-phase': 'active', + 'release-type': 'lts' + }, + { + 'channel-version': '7.0', + 'support-phase': 'eol', + 'release-type': 'sts' + } + ] + }; + + it('should resolve "latest" to highest stable version by default', async () => { + getJsonMock.mockResolvedValue({result: mockReleases}); + + const resolver = new DotnetVersionResolver('latest'); + const version = await resolver.createDotnetVersion(); + + expect(version.value).toBe('9.0'); + expect(version.type.toLowerCase()).toContain('channel'); + expect(version.qualityFlag).toBe(true); + }); + + it('should resolve "LATEST" (uppercase) to highest stable version', async () => { + getJsonMock.mockResolvedValue({result: mockReleases}); + + const resolver = new DotnetVersionResolver('LATEST'); + const version = await resolver.createDotnetVersion(); + + expect(version.value).toBe('9.0'); + expect(version.type.toLowerCase()).toContain('channel'); + expect(version.qualityFlag).toBe(true); + }); + + it('should resolve "latest" to highest preview version if quality is preview', async () => { + getJsonMock.mockResolvedValue({result: mockReleases}); + + const resolver = new DotnetVersionResolver('latest', 'preview'); + const version = await resolver.createDotnetVersion(); + + expect(version.value).toBe('10.0'); + }); + + it('should resolve "latest" with channel filter LTS', async () => { + getJsonMock.mockResolvedValue({result: mockReleases}); + + const resolver = new DotnetVersionResolver('latest', '', 'LTS'); + const version = await resolver.createDotnetVersion(); + + expect(version.value).toBe('8.0'); + }); + + it('should resolve "latest" with channel filter STS', async () => { + getJsonMock.mockResolvedValue({result: mockReleases}); + + const resolver = new DotnetVersionResolver('latest', '', 'STS'); + const version = await resolver.createDotnetVersion(); + + expect(version.value).toBe('9.0'); + }); + + it('should resolve "latest" with channel filter STS and preview quality', async () => { + getJsonMock.mockResolvedValue({result: mockReleases}); + + const resolver = new DotnetVersionResolver('latest', 'preview', 'STS'); + const version = await resolver.createDotnetVersion(); + + // preview quality includes all support-phases; STS filter → 9.0 (active, sts) + expect(version.value).toBe('9.0'); + }); + + it('should warn if channel is provided but version is not latest', async () => { + getJsonMock.mockResolvedValue({result: mockReleases}); + + const resolver = new DotnetVersionResolver('8.0', '', 'LTS'); + await resolver.createDotnetVersion(); + + expect(warningSpy).toHaveBeenCalledWith( + `The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.` + ); + }); + + it('should throw when releases-index API returns empty active releases', async () => { + const emptyReleases = { + 'releases-index': [ + { + 'channel-version': '7.0', + 'support-phase': 'eol', + 'release-type': 'sts' + } + ] + }; + getJsonMock.mockResolvedValue({result: emptyReleases}); + + const resolver = new DotnetVersionResolver('latest'); + + await expect(resolver.createDotnetVersion()).rejects.toThrow( + /Could not find any active releases/ + ); + }); + + it('should throw when releases-index response has unexpected format', async () => { + getJsonMock.mockResolvedValue({result: {}}); + + const resolver = new DotnetVersionResolver('latest'); + + await expect(resolver.createDotnetVersion()).rejects.toThrow( + /Unexpected response format/ + ); + }); + + it('should throw when releases-index response is null', async () => { + getJsonMock.mockResolvedValue({result: null}); + + const resolver = new DotnetVersionResolver('latest'); + + await expect(resolver.createDotnetVersion()).rejects.toThrow( + /Unexpected response format/ + ); + }); + + it('should resolve "latest" with ga quality same as default (no previews)', async () => { + getJsonMock.mockResolvedValue({result: mockReleases}); + + const resolver = new DotnetVersionResolver('latest', 'ga'); + const version = await resolver.createDotnetVersion(); + + // ga should behave like no quality — skip preview (10.0), pick 9.0 + expect(version.value).toBe('9.0'); + }); + + it('should resolve "latest" with LTS channel and daily quality', async () => { + getJsonMock.mockResolvedValue({result: mockReleases}); + + const resolver = new DotnetVersionResolver('latest', 'daily', 'LTS'); + const version = await resolver.createDotnetVersion(); + + // daily allows previews, LTS filter applies — 10.0 (preview, lts) is the highest LTS + expect(version.value).toBe('10.0'); + expect(version.qualityFlag).toBe(true); + }); + + it('should resolve "latest" with A.B channel directly without API call', async () => { + const resolver = new DotnetVersionResolver('latest', '', '8.0'); + const version = await resolver.createDotnetVersion(); + + expect(version.value).toBe('8.0'); + expect(version.type.toLowerCase()).toContain('channel'); + expect(version.qualityFlag).toBe(true); + // Should NOT call the API + expect(getJsonMock).not.toHaveBeenCalled(); + }); + + it('should resolve "latest" with A.B.Cxx channel directly without API call', async () => { + const resolver = new DotnetVersionResolver('latest', '', '8.0.1xx'); + const version = await resolver.createDotnetVersion(); + + expect(version.value).toBe('8.0.1xx'); + expect(version.type.toLowerCase()).toContain('channel'); + expect(version.qualityFlag).toBe(true); + // Should NOT call the API + expect(getJsonMock).not.toHaveBeenCalled(); + }); + + it('should resolve "latest" with A.B channel for older version with qualityFlag false', async () => { + const resolver = new DotnetVersionResolver('latest', '', '3.1'); + const version = await resolver.createDotnetVersion(); + + expect(version.value).toBe('3.1'); + expect(version.type.toLowerCase()).toContain('channel'); + // major 3 < 6 → qualityFlag false + expect(version.qualityFlag).toBe(false); + expect(getJsonMock).not.toHaveBeenCalled(); + }); + + it('should resolve "latest" with A.B.Cxx channel and quality', async () => { + const resolver = new DotnetVersionResolver('latest', 'ga', '8.0.2xx'); + const version = await resolver.createDotnetVersion(); + + expect(version.value).toBe('8.0.2xx'); + expect(version.qualityFlag).toBe(true); + expect(getJsonMock).not.toHaveBeenCalled(); + }); +}); diff --git a/__tests__/setup-dotnet.test.ts b/__tests__/setup-dotnet.test.ts index 5f01d55..7c05d38 100644 --- a/__tests__/setup-dotnet.test.ts +++ b/__tests__/setup-dotnet.test.ts @@ -84,7 +84,7 @@ describe('setup-dotnet tests', () => { inputs['dotnet-version'] = ['10.0']; inputs['dotnet-quality'] = 'fictitiousQuality'; - const expectedErrorMessage = `Value '${inputs['dotnet-quality']}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`; + const expectedErrorMessage = `Value '${inputs['dotnet-quality']}' is not supported for the 'dotnet-quality' option. Supported values are: daily, preview, ga.`; await setup.run(); expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage); @@ -256,5 +256,95 @@ describe('setup-dotnet tests', () => { await setup.run(); expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage); }); + + it('should fail the action if unsupported dotnet-channel value is provided with latest', async () => { + inputs['dotnet-version'] = ['latest']; + inputs['dotnet-quality'] = ''; + inputs['dotnet-channel'] = 'invalid'; + inputs['architecture'] = ''; + + const expectedErrorMessage = `Value 'invalid' is not supported for the 'dotnet-channel' option. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).`; + + await setup.run(); + expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage); + }); + + it('should warn but not fail if unsupported dotnet-channel value is provided with a specific version', async () => { + inputs['dotnet-version'] = ['8.0.x']; + inputs['dotnet-quality'] = ''; + inputs['dotnet-channel'] = 'invalid'; + inputs['architecture'] = ''; + + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + + await setup.run(); + expect(setFailedSpy).not.toHaveBeenCalled(); + expect(warningSpy).toHaveBeenCalledWith( + `Value 'invalid' is not supported for the 'dotnet-channel' option and will be ignored because 'dotnet-version' is not set to 'latest'. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).` + ); + }); + + it('should pass valid dotnet-channel value through without error', async () => { + inputs['dotnet-version'] = ['latest']; + inputs['dotnet-quality'] = ''; + inputs['dotnet-channel'] = 'LTS'; + inputs['architecture'] = ''; + + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + + await setup.run(); + expect(setFailedSpy).not.toHaveBeenCalled(); + }); + + it('should pass A.B channel value through without error when used with latest', async () => { + inputs['dotnet-version'] = ['latest']; + inputs['dotnet-quality'] = ''; + inputs['dotnet-channel'] = '8.0'; + inputs['architecture'] = ''; + + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + + await setup.run(); + expect(setFailedSpy).not.toHaveBeenCalled(); + }); + + it('should pass A.B.Cxx channel value through without error when used with latest', async () => { + inputs['dotnet-version'] = ['latest']; + inputs['dotnet-quality'] = ''; + inputs['dotnet-channel'] = '8.0.1xx'; + inputs['architecture'] = ''; + + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + + await setup.run(); + expect(setFailedSpy).not.toHaveBeenCalled(); + }); + + it('should fail with A.B.Cxx channel if major version is below 5', async () => { + inputs['dotnet-version'] = ['latest']; + inputs['dotnet-quality'] = ''; + inputs['dotnet-channel'] = '3.1.1xx'; + inputs['architecture'] = ''; + + const expectedErrorMessage = `Value '3.1.1xx' is not supported for the 'dotnet-channel' option. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).`; + + await setup.run(); + expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage); + }); + + it('should warn and not fail if valid dotnet-channel is provided with a non-latest version', async () => { + inputs['dotnet-version'] = ['8.0.x']; + inputs['dotnet-quality'] = ''; + inputs['dotnet-channel'] = 'LTS'; + inputs['architecture'] = ''; + + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + + await setup.run(); + expect(setFailedSpy).not.toHaveBeenCalled(); + expect(warningSpy).toHaveBeenCalledWith( + `The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.` + ); + }); }); }); diff --git a/action.yml b/action.yml index 861fb9d..315bc8f 100644 --- a/action.yml +++ b/action.yml @@ -6,9 +6,11 @@ branding: color: green inputs: dotnet-version: - description: 'Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x, 3.x, 6.0.2xx' + description: 'Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x, 3.x, 6.0.2xx, latest' dotnet-quality: - description: 'Optional quality of the build. The possible values are: daily, signed, validated, preview, ga.' + description: 'Optional quality of the build. The possible values are: daily, preview, ga.' + dotnet-channel: + description: 'Optional channel for the installation. The possible values are: STS, LTS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx, available since 5.0). To be used with "dotnet-version: latest".' global-json-file: description: 'Optional global.json location, if your global.json isn''t located in the root of the repo.' source-url: @@ -39,4 +41,4 @@ runs: using: 'node24' main: 'dist/setup/index.js' post: 'dist/cache-save/index.js' - post-if: success() + post-if: success() \ No newline at end of file diff --git a/dist/setup/index.js b/dist/setup/index.js index b6aece1..c58f527 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -78690,15 +78690,51 @@ const utils_1 = __nccwpck_require__(71314); const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6; const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5; class DotnetVersionResolver { + quality; + dotnetChannel; inputVersion; resolvedArgument; - constructor(version) { + constructor(version, quality = '', dotnetChannel) { + this.quality = quality; + this.dotnetChannel = dotnetChannel; this.inputVersion = version.trim(); this.resolvedArgument = { type: '', value: '', qualityFlag: false }; } + isVersionChannel(channel) { + // A.B format (e.g., 3.1, 8.0) + if (/^\d+\.\d+$/.test(channel)) + return true; + // A.B.Cxx format (e.g., 8.0.1xx) is supported only for .NET 5.0+ + const latestPatchMatch = channel.match(/^(\d+)\.\d+\.\d{1}xx$/); + if (latestPatchMatch) { + const major = Number(latestPatchMatch[1]); + return (!Number.isNaN(major) && major >= LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG); + } + return false; + } async resolveVersionInput() { + if (this.inputVersion.toLowerCase() === 'latest') { + const channel = this.dotnetChannel || ''; + if (this.isVersionChannel(channel)) { + // A.B or A.B.Cxx channels are passed directly to the install script + this.resolvedArgument.value = channel; + } + else { + // LTS, STS, or empty — resolve via releases index API + this.resolvedArgument.value = await this.getLatestVersion(channel); + } + this.resolvedArgument.type = 'channel'; + const latestChannelMajorTag = Number(this.resolvedArgument.value.split('.')[0]); + this.resolvedArgument.qualityFlag = + !Number.isNaN(latestChannelMajorTag) && + latestChannelMajorTag >= QUALITY_INPUT_MINIMAL_MAJOR_TAG; + return; + } + if (this.dotnetChannel) { + core.warning(`The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.`); + } if (!semver_1.default.validRange(this.inputVersion) && !this.isLatestPatchSyntax()) { - throw new Error(`The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x, A.B.Cxx`); + throw new Error(`The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x, A.B.Cxx, latest`); } if (semver_1.default.valid(this.inputVersion)) { this.createVersionArgument(); @@ -78756,6 +78792,46 @@ class DotnetVersionResolver { } return this.resolvedArgument; } + async getLatestVersion(channelFilter) { + const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { + allowRetries: true, + maxRetries: 3 + }); + const response = await httpClient.getJson(DotnetVersionResolver.DotnetCoreIndexUrl); + const result = response.result; + const rawReleasesInfo = result?.['releases-index']; + if (!Array.isArray(rawReleasesInfo)) { + throw new Error('Unexpected response format from .NET releases index.'); + } + let releasesInfo = rawReleasesInfo; + // Filter out EOL versions + releasesInfo = releasesInfo.filter(info => info['support-phase'] !== 'eol'); + // Filter out preview versions if quality is not 'preview' or 'daily' + // If quality is not specified, we assume strict stability (GA only) + const normalizedQuality = (this.quality || '').toLowerCase(); + if (!['preview', 'daily'].includes(normalizedQuality)) { + releasesInfo = releasesInfo.filter(info => info['support-phase'] !== 'preview'); + } + // Apply channel filter (LTS/STS) + if (channelFilter) { + const type = channelFilter.toLowerCase(); + releasesInfo = releasesInfo.filter(info => info['release-type'] === type); + } + releasesInfo.sort((a, b) => { + const partsA = a['channel-version'].split('.').map(Number); + const partsB = b['channel-version'].split('.').map(Number); + for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { + const diff = (partsB[i] || 0) - (partsA[i] || 0); + if (diff !== 0) + return diff; + } + return 0; + }); + if (releasesInfo.length === 0) { + throw new Error(`Could not find any active releases matching channel '${channelFilter || 'any'}'`); + } + return releasesInfo[0]['channel-version']; + } async getLatestByMajorTag(majorTag) { const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { allowRetries: true, @@ -78891,16 +78967,18 @@ class DotnetCoreInstaller { version; quality; architecture; + dotnetChannel; static { DotnetInstallDir.setEnvironmentVariable(); } - constructor(version, quality, architecture) { + constructor(version, quality, architecture, dotnetChannel) { this.version = version; this.quality = quality; this.architecture = architecture; + this.dotnetChannel = dotnetChannel; } async installDotnet() { - const versionResolver = new DotnetVersionResolver(this.version); + const versionResolver = new DotnetVersionResolver(this.version, this.quality, this.dotnetChannel); const dotnetVersion = await versionResolver.createDotnetVersion(); const architectureArguments = this.architecture && normalizeArch(this.architecture) !== normalizeArch(os_1.default.arch()) @@ -79019,13 +79097,7 @@ const cache_utils_1 = __nccwpck_require__(41678); const cache_restore_1 = __nccwpck_require__(19517); const constants_1 = __nccwpck_require__(69042); const json5_1 = __importDefault(__nccwpck_require__(86904)); -const qualityOptions = [ - 'daily', - 'signed', - 'validated', - 'preview', - 'ga' -]; +const qualityOptions = ['daily', 'preview', 'ga']; const supportedArchitectures = [ 'x64', 'x86', @@ -79036,6 +79108,19 @@ const supportedArchitectures = [ 'ppc64le', 'riscv64' ]; +function isValidChannel(channel) { + const upper = channel.toUpperCase(); + if (upper === 'LTS' || upper === 'STS') + return true; + // A.B format (e.g., 3.1, 8.0) + if (/^\d+\.\d+$/.test(channel)) + return true; + // A.B.Cxx format (e.g., 8.0.1xx) - available since 5.0 + const match = channel.match(/^(?\d+)\.\d+\.\d{1}xx$/); + if (match && parseInt(match.groups.major) >= 5) + return true; + return false; +} async function run() { try { // @@ -79050,6 +79135,21 @@ async function run() { const versions = core.getMultilineInput('dotnet-version'); const installedDotnetVersions = []; const architecture = getArchitectureInput(); + let dotnetChannel = core.getInput('dotnet-channel'); + const isLatestRequested = versions.some(version => version && version.toLowerCase() === 'latest'); + if (dotnetChannel && !isValidChannel(dotnetChannel)) { + if (isLatestRequested) { + throw new Error(`Value '${dotnetChannel}' is not supported for the 'dotnet-channel' option. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).`); + } + else { + core.warning(`Value '${dotnetChannel}' is not supported for the 'dotnet-channel' option and will be ignored because 'dotnet-version' is not set to 'latest'. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).`); + dotnetChannel = ''; + } + } + else if (dotnetChannel && !isLatestRequested) { + core.warning(`The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.`); + dotnetChannel = ''; + } const globalJsonFileInput = core.getInput('global-json-file'); if (globalJsonFileInput) { const globalJsonPath = path_1.default.resolve(process.cwd(), globalJsonFileInput); @@ -79072,12 +79172,12 @@ async function run() { if (versions.length) { const quality = core.getInput('dotnet-quality'); if (quality && !qualityOptions.includes(quality)) { - throw new Error(`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`); + throw new Error(`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, preview, ga.`); } let dotnetInstaller; - const uniqueVersions = new Set(versions); + const uniqueVersions = new Set(versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v))); for (const version of uniqueVersions) { - dotnetInstaller = new installer_1.DotnetCoreInstaller(version, quality, architecture); + dotnetInstaller = new installer_1.DotnetCoreInstaller(version, quality, architecture, version.toLowerCase() === 'latest' ? dotnetChannel : undefined); const installedVersion = await dotnetInstaller.installDotnet(); installedDotnetVersions.push(installedVersion); } diff --git a/src/installer.ts b/src/installer.ts index 451c66c..2402316 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -16,21 +16,74 @@ export interface DotnetVersion { qualityFlag: boolean; } +interface ReleaseIndexEntry { + 'channel-version': string; + 'support-phase': string; + 'release-type': string; +} + +interface ReleaseIndexResponse { + 'releases-index': ReleaseIndexEntry[]; +} + const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6; const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5; export class DotnetVersionResolver { private inputVersion: string; private resolvedArgument: DotnetVersion; - constructor(version: string) { + constructor( + version: string, + private quality: QualityOptions = '', + private dotnetChannel?: string + ) { this.inputVersion = version.trim(); this.resolvedArgument = {type: '', value: '', qualityFlag: false}; } + private isVersionChannel(channel: string): boolean { + // A.B format (e.g., 3.1, 8.0) + if (/^\d+\.\d+$/.test(channel)) return true; + // A.B.Cxx format (e.g., 8.0.1xx) is supported only for .NET 5.0+ + const latestPatchMatch = channel.match(/^(\d+)\.\d+\.\d{1}xx$/); + if (latestPatchMatch) { + const major = Number(latestPatchMatch[1]); + return ( + !Number.isNaN(major) && major >= LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG + ); + } + return false; + } + private async resolveVersionInput(): Promise { + if (this.inputVersion.toLowerCase() === 'latest') { + const channel = this.dotnetChannel || ''; + if (this.isVersionChannel(channel)) { + // A.B or A.B.Cxx channels are passed directly to the install script + this.resolvedArgument.value = channel; + } else { + // LTS, STS, or empty — resolve via releases index API + this.resolvedArgument.value = await this.getLatestVersion(channel); + } + this.resolvedArgument.type = 'channel'; + const latestChannelMajorTag = Number( + this.resolvedArgument.value.split('.')[0] + ); + this.resolvedArgument.qualityFlag = + !Number.isNaN(latestChannelMajorTag) && + latestChannelMajorTag >= QUALITY_INPUT_MINIMAL_MAJOR_TAG; + return; + } + + if (this.dotnetChannel) { + core.warning( + `The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.` + ); + } + if (!semver.validRange(this.inputVersion) && !this.isLatestPatchSyntax()) { throw new Error( - `The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x, A.B.Cxx` + `The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x, A.B.Cxx, latest` ); } if (semver.valid(this.inputVersion)) { @@ -96,6 +149,64 @@ export class DotnetVersionResolver { return this.resolvedArgument; } + private async getLatestVersion(channelFilter: string): Promise { + const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { + allowRetries: true, + maxRetries: 3 + }); + + const response = await httpClient.getJson( + DotnetVersionResolver.DotnetCoreIndexUrl + ); + + const result = response.result; + const rawReleasesInfo = result?.['releases-index']; + + if (!Array.isArray(rawReleasesInfo)) { + throw new Error('Unexpected response format from .NET releases index.'); + } + + let releasesInfo = rawReleasesInfo; + + // Filter out EOL versions + releasesInfo = releasesInfo.filter(info => info['support-phase'] !== 'eol'); + + // Filter out preview versions if quality is not 'preview' or 'daily' + // If quality is not specified, we assume strict stability (GA only) + const normalizedQuality = (this.quality || '').toLowerCase(); + if (!['preview', 'daily'].includes(normalizedQuality)) { + releasesInfo = releasesInfo.filter( + info => info['support-phase'] !== 'preview' + ); + } + + // Apply channel filter (LTS/STS) + if (channelFilter) { + const type = channelFilter.toLowerCase(); + releasesInfo = releasesInfo.filter(info => info['release-type'] === type); + } + + releasesInfo.sort((a, b) => { + const partsA = a['channel-version'].split('.').map(Number); + const partsB = b['channel-version'].split('.').map(Number); + for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { + const diff = (partsB[i] || 0) - (partsA[i] || 0); + if (diff !== 0) return diff; + } + return 0; + }); + + if (releasesInfo.length === 0) { + throw new Error( + `Could not find any active releases matching channel '${ + channelFilter || 'any' + }'` + ); + } + + return releasesInfo[0]['channel-version']; + } + private async getLatestByMajorTag(majorTag: string): Promise { const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { allowRetries: true, @@ -279,11 +390,16 @@ export class DotnetCoreInstaller { constructor( private version: string, private quality: QualityOptions, - private architecture?: string + private architecture?: string, + private dotnetChannel?: string ) {} public async installDotnet(): Promise { - const versionResolver = new DotnetVersionResolver(this.version); + const versionResolver = new DotnetVersionResolver( + this.version, + this.quality, + this.dotnetChannel + ); const dotnetVersion = await versionResolver.createDotnetVersion(); const architectureArguments = diff --git a/src/setup-dotnet.ts b/src/setup-dotnet.ts index 5a57680..4950f8f 100644 --- a/src/setup-dotnet.ts +++ b/src/setup-dotnet.ts @@ -15,13 +15,7 @@ import {restoreCache} from './cache-restore'; import {Outputs} from './constants'; import JSON5 from 'json5'; -const qualityOptions = [ - 'daily', - 'signed', - 'validated', - 'preview', - 'ga' -] as const; +const qualityOptions = ['daily', 'preview', 'ga'] as const; const supportedArchitectures = [ 'x64', 'x86', @@ -34,7 +28,18 @@ const supportedArchitectures = [ ] as const; type SupportedArchitecture = (typeof supportedArchitectures)[number]; -export type QualityOptions = (typeof qualityOptions)[number]; +export type QualityOptions = (typeof qualityOptions)[number] | ''; + +function isValidChannel(channel: string): boolean { + const upper = channel.toUpperCase(); + if (upper === 'LTS' || upper === 'STS') return true; + // A.B format (e.g., 3.1, 8.0) + if (/^\d+\.\d+$/.test(channel)) return true; + // A.B.Cxx format (e.g., 8.0.1xx) - available since 5.0 + const match = channel.match(/^(?\d+)\.\d+\.\d{1}xx$/); + if (match && parseInt(match.groups!.major) >= 5) return true; + return false; +} export async function run() { try { @@ -50,6 +55,28 @@ export async function run() { const versions = core.getMultilineInput('dotnet-version'); const installedDotnetVersions: (string | null)[] = []; const architecture = getArchitectureInput(); + let dotnetChannel = core.getInput('dotnet-channel'); + + const isLatestRequested = versions.some( + version => version && version.toLowerCase() === 'latest' + ); + if (dotnetChannel && !isValidChannel(dotnetChannel)) { + if (isLatestRequested) { + throw new Error( + `Value '${dotnetChannel}' is not supported for the 'dotnet-channel' option. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).` + ); + } else { + core.warning( + `Value '${dotnetChannel}' is not supported for the 'dotnet-channel' option and will be ignored because 'dotnet-version' is not set to 'latest'. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).` + ); + dotnetChannel = ''; + } + } else if (dotnetChannel && !isLatestRequested) { + core.warning( + `The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.` + ); + dotnetChannel = ''; + } const globalJsonFileInput = core.getInput('global-json-file'); if (globalJsonFileInput) { @@ -80,17 +107,20 @@ export async function run() { if (quality && !qualityOptions.includes(quality)) { throw new Error( - `Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.` + `Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, preview, ga.` ); } let dotnetInstaller: DotnetCoreInstaller; - const uniqueVersions = new Set(versions); + const uniqueVersions = new Set( + versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v)) + ); for (const version of uniqueVersions) { dotnetInstaller = new DotnetCoreInstaller( version, quality, - architecture + architecture, + version.toLowerCase() === 'latest' ? dotnetChannel : undefined ); const installedVersion = await dotnetInstaller.installDotnet(); installedDotnetVersions.push(installedVersion); From f1970f5ca30a3ba974274132e05c4bb012faac74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=B6plinger?= Date: Mon, 11 May 2026 19:11:45 +0200 Subject: [PATCH 2/5] Don't download releases-index.json to resolve major version (#560) * Don't download releases-index.json to resolving major version Starting with .NET 5 the minor version is always zero. The earlier releases don't get new versions anymore so we can hardcode the minor version instead of downloading releases-index.json to do the lookup. * Remove unused variable * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Commit dist/setup/index.js * Refresh after copilot commits --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dist/setup/index.js | 34 ++++++++++++++++------------------ src/installer.ts | 44 ++++++++++++++++---------------------------- 2 files changed, 32 insertions(+), 46 deletions(-) diff --git a/dist/setup/index.js b/dist/setup/index.js index c58f527..ac2cc6c 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -78768,7 +78768,22 @@ class DotnetVersionResolver { this.resolvedArgument.value = `${major}.${minor}`; } else if (this.isNumericTag(major)) { - this.resolvedArgument.value = await this.getLatestByMajorTag(major); + // Starting with .NET 5, the minor version is always zero. + // Hardcode the earlier versions because they will not get new releases. + switch (major) { + case '1': + this.resolvedArgument.value = '1.1'; + break; + case '2': + this.resolvedArgument.value = '2.2'; + break; + case '3': + this.resolvedArgument.value = '3.1'; + break; + default: + this.resolvedArgument.value = `${major}.0`; + break; + } } else { // If "dotnet-version" is specified as *, x or X resolve latest version of .NET explicitly from LTS channel. The version argument will default to "latest" by install-dotnet script. @@ -78832,23 +78847,6 @@ class DotnetVersionResolver { } return releasesInfo[0]['channel-version']; } - async getLatestByMajorTag(majorTag) { - const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { - allowRetries: true, - maxRetries: 3 - }); - const response = await httpClient.getJson(DotnetVersionResolver.DotnetCoreIndexUrl); - const result = response.result || {}; - const releasesInfo = result['releases-index']; - const releaseInfo = releasesInfo.find(info => { - const sdkParts = info['channel-version'].split('.'); - return sdkParts[0] === majorTag; - }); - if (!releaseInfo) { - throw new Error(`Could not find info for version with major tag: "${majorTag}" at ${DotnetVersionResolver.DotnetCoreIndexUrl}`); - } - return releaseInfo['channel-version']; - } static DotnetCoreIndexUrl = 'https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json'; } exports.DotnetVersionResolver = DotnetVersionResolver; diff --git a/src/installer.ts b/src/installer.ts index 2402316..85a2d82 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -125,7 +125,22 @@ export class DotnetVersionResolver { } else if (this.isNumericTag(major) && this.isNumericTag(minor)) { this.resolvedArgument.value = `${major}.${minor}`; } else if (this.isNumericTag(major)) { - this.resolvedArgument.value = await this.getLatestByMajorTag(major); + // Starting with .NET 5, the minor version is always zero. + // Hardcode the earlier versions because they will not get new releases. + switch (major) { + case '1': + this.resolvedArgument.value = '1.1'; + break; + case '2': + this.resolvedArgument.value = '2.2'; + break; + case '3': + this.resolvedArgument.value = '3.1'; + break; + default: + this.resolvedArgument.value = `${major}.0`; + break; + } } else { // If "dotnet-version" is specified as *, x or X resolve latest version of .NET explicitly from LTS channel. The version argument will default to "latest" by install-dotnet script. this.resolvedArgument.value = 'LTS'; @@ -207,33 +222,6 @@ export class DotnetVersionResolver { return releasesInfo[0]['channel-version']; } - private async getLatestByMajorTag(majorTag: string): Promise { - const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { - allowRetries: true, - maxRetries: 3 - }); - - const response = await httpClient.getJson( - DotnetVersionResolver.DotnetCoreIndexUrl - ); - - const result = response.result || {}; - const releasesInfo: any[] = result['releases-index']; - - const releaseInfo = releasesInfo.find(info => { - const sdkParts: string[] = info['channel-version'].split('.'); - return sdkParts[0] === majorTag; - }); - - if (!releaseInfo) { - throw new Error( - `Could not find info for version with major tag: "${majorTag}" at ${DotnetVersionResolver.DotnetCoreIndexUrl}` - ); - } - - return releaseInfo['channel-version']; - } - static DotnetCoreIndexUrl = 'https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json'; } From 84042723628274fd730a70726fbf57ef89e586de Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 26 May 2026 12:22:17 -0500 Subject: [PATCH 3/5] Update install scripts to v2026.05.19 (#736) Update externals/install-dotnet.ps1 and externals/install-dotnet.sh to the v2026.05.19 release of dotnet/install-scripts. This uptakes the changes from dotnet/install-scripts#697 which preserves archive links during installation (hardlinks on Windows, symlinks on Linux) as part of dotnet/sdk#52182. On Windows, .NET 11.0+ is now installed via tar.gz archives which preserves hardlinks, significantly reducing disk space usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- externals/install-dotnet.ps1 | 570 +++++++++++++++++++++-------------- externals/install-dotnet.sh | 10 +- 2 files changed, 353 insertions(+), 227 deletions(-) diff --git a/externals/install-dotnet.ps1 b/externals/install-dotnet.ps1 index 8537c60..3fa9025 100644 --- a/externals/install-dotnet.ps1 +++ b/externals/install-dotnet.ps1 @@ -94,9 +94,9 @@ Determines timeout duration in seconds for downloading of the SDK file Default: 1200 seconds (20 minutes) .PARAMETER KeepZip - If set, downloaded file is kept + If set, downloaded archive file is kept. Applies to both .zip and .tar.gz formats. .PARAMETER ZipPath - Use that path to store installer, generated by default + Use that path to store the downloaded archive, generated by default. Applies to both .zip and .tar.gz formats. .EXAMPLE dotnet-install.ps1 -Version 7.0.401 Installs the .NET SDK version 7.0.401 @@ -341,6 +341,36 @@ function Get-NormalizedProduct([string]$Runtime) { } } +function Test-TarAvailable { + if ($env:DOTNET_INSTALL_SKIP_TAR -eq "1") { + Say-Verbose "Skipping tar detection due to DOTNET_INSTALL_SKIP_TAR environment variable." + return $false + } + $tarCommand = Get-Command -Name "tar" -ErrorAction SilentlyContinue + return $null -ne $tarCommand +} + +function Get-FileExtension-For-Version([string]$VersionOrChannel) { + if (-not $script:TarAvailable) { + return ".zip" + } + if ($VersionOrChannel -match '^(\d+)\.') { + $majorVersion = [int]$Matches[1] + if ($majorVersion -ge 11) { + # Windows tarballs are only available starting with 11.0 preview 3 + if ($VersionOrChannel -match 'preview\.(\d+)') { + $previewNum = [int]$Matches[1] + if ($majorVersion -eq 11 -and $previewNum -lt 3) { + Say-Verbose "Version '$VersionOrChannel' predates tar.gz availability; using zip." + return ".zip" + } + } + Say-Verbose "Using tar.gz archive format for version/channel '$VersionOrChannel'." + return ".tar.gz" + } + } + return ".zip" +} # The version text returned from the feeds is a 1-line or 2-line string: # For the SDK and the dotnet runtime (2 lines): @@ -594,24 +624,27 @@ function Get-Download-Link([string]$AzureFeed, [string]$SpecificVersion, [string # If anything fails in this lookup it will default to $SpecificVersion $SpecificProductVersion = Get-Product-Version -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion + # For .NET 11.0+, tar.gz is the preferred archive format on Windows + $ext = Get-FileExtension-For-Version $SpecificVersion + if ($Runtime -eq "dotnet") { - $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificProductVersion-win-$CLIArchitecture$ext" } elseif ($Runtime -eq "aspnetcore") { - $PayloadURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + $PayloadURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$SpecificProductVersion-win-$CLIArchitecture$ext" } 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" + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture$ext" if ($SpecificVersion -match '^(\d+)\.(.*)$') { $majorVersion = [int]$Matches[1] if ($majorVersion -ge 5) { - $PayloadURL = "$AzureFeed/WindowsDesktop/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + $PayloadURL = "$AzureFeed/WindowsDesktop/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture$ext" } } } elseif (-not $Runtime) { - $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificProductVersion-win-$CLIArchitecture.zip" + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificProductVersion-win-$CLIArchitecture$ext" } else { throw "Invalid value for `$Runtime" @@ -877,6 +910,80 @@ function Extract-Dotnet-Package([string]$ZipPath, [string]$OutPath) { } } +function Extract-Dotnet-Package-Tar([string]$TarPath, [string]$OutPath) { + Say-Invocation $MyInvocation + + New-Item -ItemType Directory -Force -Path $OutPath | Out-Null + + # Build an exclude list of versioned directories that already exist locally. + # This matches the zip extraction behavior: if shared/Microsoft.NETCore.App/11.0.0/ + # already exists, we skip all files under that version to avoid overwriting a + # previously installed version. + $excludeArgs = @() + $versionRegex = '.*/\d+\.\d+[^/]+/' + + # Read the tarball's file listing to discover versioned directory prefixes + $tarListing = & tar -tzf $TarPath 2>$null + $versionedDirs = @{} + foreach ($entry in $tarListing) { + $normalizedEntry = $entry.Replace('\', '/') + $match = [regex]::Match($normalizedEntry, $versionRegex) + if ($match.Success) { + $versionedPrefix = $normalizedEntry.Substring(0, $match.Index + $match.Length) + if (-not $versionedDirs.ContainsKey($versionedPrefix)) { + $localDir = Join-Path -Path $OutPath -ChildPath $versionedPrefix + $versionedDirs[$versionedPrefix] = (Test-Path $localDir -PathType Container) + } + } + } + + foreach ($dir in $versionedDirs.GetEnumerator()) { + if ($dir.Value) { + # This versioned directory already exists — exclude it from extraction + $pattern = $dir.Key + '*' + $excludeArgs += '--exclude' + $excludeArgs += $pattern + Say-Verbose "Excluding pre-existing versioned directory: $($dir.Key)" + } + } + + # Handle non-versioned file override logic. + # If OverrideNonVersionedFiles is false, exclude non-versioned files that already exist. + if (-not $OverrideNonVersionedFiles) { + foreach ($entry in $tarListing) { + $normalizedEntry = $entry.Replace('\', '/') + # Skip directory entries (end with /) + if ($normalizedEntry.EndsWith('/')) { continue } + $match = [regex]::Match($normalizedEntry, $versionRegex) + if (-not $match.Success) { + # Non-versioned file — exclude if it already exists locally + $localPath = Join-Path -Path $OutPath -ChildPath $normalizedEntry + if (Test-Path $localPath) { + $excludeArgs += '--exclude' + $excludeArgs += $normalizedEntry + Say-Verbose "Excluding pre-existing non-versioned file: $normalizedEntry" + } + } + } + } + + try { + # Extract directly to install root, preserving hard links + $tarOutput = & tar -xzf $TarPath -C $OutPath @excludeArgs 2>&1 + if ($LASTEXITCODE -ne 0) { + $tarOutputText = ($tarOutput | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($tarOutputText)) { + throw "Tar extraction failed with exit code $LASTEXITCODE." + } + throw "Tar extraction failed with exit code $LASTEXITCODE. tar output: $tarOutputText" + } + } + catch { + Say-Error "Failed to extract tar package. Exception: $_" + throw + } +} + function DownloadFile($Source, [string]$OutPath) { if ($Source -notlike "http*") { # Using System.IO.Path.GetFullPath to get the current directory @@ -991,7 +1098,7 @@ function PrintDryRunOutput($Invocation, $DownloadLinks) { } } -function Get-AkaMSDownloadLink([string]$Channel, [string]$Quality, [bool]$Internal, [string]$Product, [string]$Architecture) { +function Get-AkaMSDownloadLink([string]$Channel, [string]$Quality, [bool]$Internal, [string]$Product, [string]$Architecture, [string]$FileExtension = ".zip") { Say-Invocation $MyInvocation #quality is not supported for LTS or STS channel @@ -1010,7 +1117,7 @@ function Get-AkaMSDownloadLink([string]$Channel, [string]$Quality, [bool]$Intern if (-not [string]::IsNullOrEmpty($Quality)) { $akaMsLink += "/$Quality" } - $akaMsLink += "/$Product-win-$Architecture.zip" + $akaMsLink += "/$Product-win-$Architecture$FileExtension" Say-Verbose "Constructed aka.ms link: '$akaMsLink'." $akaMsDownloadLink = $null @@ -1062,7 +1169,23 @@ function Get-AkaMSDownloadLink([string]$Channel, [string]$Quality, [bool]$Intern } 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 + + # When tar is available, try .tar.gz first via aka.ms, then fall back to .zip. + # This handles symbolic channels (STS/LTS) and numeric channels where tar.gz + # may not yet be available (e.g. pre-11.0 versions). + $extensionsToTry = @(".zip") + if ($script:TarAvailable) { + $extensionsToTry = @(".tar.gz", ".zip") + } + + $AkaMsDownloadLink = $null + foreach ($ext in $extensionsToTry) { + $AkaMsDownloadLink = Get-AkaMSDownloadLink -Channel $NormalizedChannel -Quality $NormalizedQuality -Internal $Internal -Product $ProductName -Architecture $Architecture -FileExtension $ext + if (-not [string]::IsNullOrEmpty($AkaMsDownloadLink)) { + break + } + Say-Verbose "aka.ms link resolution with '$ext' extension failed, trying next format." + } if ([string]::IsNullOrEmpty($AkaMsDownloadLink)) { if (-not [string]::IsNullOrEmpty($NormalizedQuality)) { @@ -1191,6 +1314,8 @@ Measure-Action "Product discovery" { $script:NormalizedProduct = Get-NormalizedProduct $Runtime Say-Verbose "Normalized product: '$NormalizedProduct'" $script:FeedCredential = ValidateFeedCredential $FeedCredential + $script:TarAvailable = Test-TarAvailable + Say-Verbose "Tar available: '$TarAvailable'" } $InstallRoot = Resolve-Installation-Path $InstallDir @@ -1319,7 +1444,12 @@ if (-not $DownloadSucceeded) { } Say "Extracting the archive." -Measure-Action "Package extraction" { Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot } +if ($DownloadedLink.downloadLink.EndsWith(".tar.gz")) { + Measure-Action "Package extraction" { Extract-Dotnet-Package-Tar -TarPath $ZipPath -OutPath $InstallRoot } +} +else { + 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 @@ -1355,219 +1485,215 @@ Say "Installed version is $($DownloadedLink.effectiveVersion)" Say "Installation finished" # SIG # Begin signature block -# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor +# MIIncQYJKoZIhvcNAQcCoIInYjCCJ14CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG -# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCyKn7B6ieM6Y2C -# rr9TCFvTSv2mMIh9mBGXh4z2gOksEqCCDXYwggX0MIID3KADAgECAhMzAAAEhV6Z -# 7A5ZL83XAAAAAASFMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD -# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy -# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p -# bmcgUENBIDIwMTEwHhcNMjUwNjE5MTgyMTM3WhcNMjYwNjE3MTgyMTM3WjB0MQsw -# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u -# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy -# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -# AQDASkh1cpvuUqfbqxele7LCSHEamVNBfFE4uY1FkGsAdUF/vnjpE1dnAD9vMOqy -# 5ZO49ILhP4jiP/P2Pn9ao+5TDtKmcQ+pZdzbG7t43yRXJC3nXvTGQroodPi9USQi -# 9rI+0gwuXRKBII7L+k3kMkKLmFrsWUjzgXVCLYa6ZH7BCALAcJWZTwWPoiT4HpqQ -# hJcYLB7pfetAVCeBEVZD8itKQ6QA5/LQR+9X6dlSj4Vxta4JnpxvgSrkjXCz+tlJ -# 67ABZ551lw23RWU1uyfgCfEFhBfiyPR2WSjskPl9ap6qrf8fNQ1sGYun2p4JdXxe -# UAKf1hVa/3TQXjvPTiRXCnJPAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE -# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUuCZyGiCuLYE0aU7j5TFqY05kko0w -# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW -# MBQGA1UEBRMNMjMwMDEyKzUwNTM1OTAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci -# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j -# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG -# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu -# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 -# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBACjmqAp2Ci4sTHZci+qk -# tEAKsFk5HNVGKyWR2rFGXsd7cggZ04H5U4SV0fAL6fOE9dLvt4I7HBHLhpGdE5Uj -# Ly4NxLTG2bDAkeAVmxmd2uKWVGKym1aarDxXfv3GCN4mRX+Pn4c+py3S/6Kkt5eS -# DAIIsrzKw3Kh2SW1hCwXX/k1v4b+NH1Fjl+i/xPJspXCFuZB4aC5FLT5fgbRKqns -# WeAdn8DsrYQhT3QXLt6Nv3/dMzv7G/Cdpbdcoul8FYl+t3dmXM+SIClC3l2ae0wO -# lNrQ42yQEycuPU5OoqLT85jsZ7+4CaScfFINlO7l7Y7r/xauqHbSPQ1r3oIC+e71 -# 5s2G3ClZa3y99aYx2lnXYe1srcrIx8NAXTViiypXVn9ZGmEkfNcfDiqGQwkml5z9 -# nm3pWiBZ69adaBBbAFEjyJG4y0a76bel/4sDCVvaZzLM3TFbxVO9BQrjZRtbJZbk -# C3XArpLqZSfx53SuYdddxPX8pvcqFuEu8wcUeD05t9xNbJ4TtdAECJlEi0vvBxlm -# M5tzFXy2qZeqPMXHSQYqPgZ9jvScZ6NwznFD0+33kbzyhOSz/WuGbAu4cHZG8gKn -# lQVT4uA2Diex9DMs2WHiokNknYlLoUeWXW1QrJLpqO82TLyKTbBM/oZHAdIc0kzo -# STro9b3+vjn2809D0+SOOCVZMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq -# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x -# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv -# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 -# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG -# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG -# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg -# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC -# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 -# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr -# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg -# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy -# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 -# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh -# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k -# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB -# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn -# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 -# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w -# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o -# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD -# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa -# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny -# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG -# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t -# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV -# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 -# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG -# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl -# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb -# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l -# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 -# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 -# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 -# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam -# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa -# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah -# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA -# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt -# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr -# /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDmNI7+9b4lSWsX +# 2kYVvqQHpZeXB6Kt5BepNOcmIqEK1aCCDMkwggYEMIID7KADAgECAhMzAAACHPrN +# xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD +# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD +# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1 +# OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE +# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD +# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB +# DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP +# oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC +# /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf +# rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j +# qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT +# xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B +# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O +# BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL +# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT +# DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw +# YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w +# cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy +# bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z +# b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl +# MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC +# AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN +# rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK +# 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK +# Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY +# BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu +# uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE +# msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz +# 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6 +# U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO +# 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD +# 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC +# EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT +# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD +# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS +# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX +# DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m +# dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ +# Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq +# lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo +# 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv +# QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a +# 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1 +# FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO +# GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7 +# ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ +# uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS +# CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm +# VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3 +# SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E +# BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX +# LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB +# Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP +# oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv +# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw +# TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv +# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC +# AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D +# 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY +# nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI +# vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6 +# aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w +# PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7 +# RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK +# /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK +# YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw +# YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT +# Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghn+MIIZ+gIBATBu +# MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x +# KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc +# +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwG +# CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZI +# hvcNAQkEMSIEIB5Na9P++kVnSeIy27N4IyRD4N0sjfWHRS1ht0NHdoK/MEIGCisG +# AQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3 +# Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAx05EI+Kk4hoAKG/8oVdr +# UIq/+sv+E0vpSyWQMTq/yrkfnPWqT2TxNVtcG3ZcQT5/36cjMe1umi1zzy178VRP +# HKH2jxMOzMqtJPsAI/yAd46EOyTBwayS+27NOvz2r3v8WV0YjuplJb1teHmJtoIf +# UVC1va6H668UMrR2Mm5l8GvtcrGmDLjq3FLTLeidco7aj7xtkwUhyXvCS/HzoF9G +# yrYW3NKqIy+KbNXTD8RbwDxKvx3ED9C1SfZi3fi/t1a2wjNsxVqf4XNsO2+JvuuO +# SGTZOoo/3jqtS22IcV5Z3In0D3+C/rigq0179KhwPtztz4+J9MU5gtGmVayF1MH1 +# M6GCF7AwghesBgorBgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kwgheF +# AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFFMIIB +# QQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCDhOUY7jtQW2mTHjZaf +# zhylI7IWKlybklUpfwDq0GnW8AIGaeuhCndSGBMyMDI2MDUxNzAwNDk0OC4wNzFa +# MASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv +# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 +# aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0 +# ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoyQTFBLTA1RTAtRDk0NzElMCMG +# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIFEKAD +# AgECAhMzAAACEKvN5BYY7zmwAAEAAAIQMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV +# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w +# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m +# dCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgxMloXDTI2MTExMzE4 +# NDgxMlowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD +# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTAr +# BgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUG +# A1UECxMeblNoaWVsZCBUU1MgRVNOOjJBMUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxN +# aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOC +# Ag8AMIICCgKCAgEAjcc4q057ZwIgpKu4pTXWLejvYEduRf+1mIpbiJEMFWWmU2xp +# ip+zK7xFxKGB1CclUXBU0/ZQZ6LG8H0gI7yvosrsPEI1DPB/XccGCvswKbAKckng +# OuGTEPGk7K/vEZa9h0Xt02b7m2n9MdIjkLrFl0pDriKyz0QHGpdh93X6+NApfE1T +# L24Vo0xkeoFGpL3rX9gXhIOF59EMnTd2o45FW/oxMgY9q0y0jGO0HrCLTCZr50e7 +# TZRSNYAy2lyKbvKI2MKlN1wLzJvZbbc//L3s1q3J6KhS0KC2VNEImYdFgVkJej4z +# ZqHfScTbx9hjFgFpVkJl4xH5VJ8tyJdXE9+vU0k9AaT2QP1Zm3WQmXedSoLjjI7L +# WznuHwnoGIXLiJMQzPqKqRIFL3wzcrDrZeWgtAdBPbipglZ5CQns6Baj5Mb6a/EZ +# C9G3faJYK5QVHeE6eLoSEwp1dz5WurLXNPsp0VWplpl/FJb8jrRT/jOoHu85qRcd +# YpgByU9W7IWPdrthmyfqeAw0omVWN5JxcogYbLo2pANJHlsMdWnxIpN5YwHbGEPC +# uosBHPk2Xd9+E/pZPQUR6v+D85eEN5A/ZM/xiPpxa8dJZ87BpTvui7/2uflUMJf2 +# Yc9ZLPgEdhQQo0LwMDSTDT48y3sV7Pdo+g5q+MqnJztN/6qt1cgUTe9u+ykCAwEA +# AaOCAUkwggFFMB0GA1UdDgQWBBSe42+FrpdF2avbUhlk86BLSH5kejAfBgNVHSME +# GDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRw +# Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1l +# LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsG +# AQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p +# Y3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMB +# Af8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDAN +# BgkqhkiG9w0BAQsFAAOCAgEAvs4rO3oo8czOrxPqnnSEkUVq718QzlrIiy7/EW7J +# mQXsJoFxHWUF0Ux0PDyKFDRXPJVv29F7kpJkBJJmcQg5HQV7blUXIMWQ1qX0KdtF +# QXI/MRL77Z+pK5x1jX+tbRkA7a5Ft7vWuRoAEi02HpFH5m/Akh/dfsbx8wOpecJb +# YvuHuy4aG0/tGzOWFCxMMNhGAIJ4qdV87JnY/uMBmiodlm+Gz357XWW5tg3HrtNZ +# XuQ0tWUv26ud4nGKJo/oLZHP75p4Rpt7dMdYKUF9AuVFBwxYZYpvgk12tfK+/yOw +# q84/fjXVCdM83Qnawtbenbk/lnbc9KsZom+GnvA4itAMUpSXFWrcRkqdUQLN+JrG +# 6fPBoV8+D8U2Q2F4XkiCR6EU9JzYKwTuvL6t3nFuxnkLdNjbTg2/yv2j3WaDuCK5 +# lSPgsndIiH6Bku2Ui3A0aUo6D9z9v+XEuBs9ioVJaOjf/z+Urqg7ESnxG0/T1dKc +# i7vLQ2XNgWFYO+/OlDjtGoma1ijX4m14N9qgrXTuWEGwgC7hhBgp3id/LAOf9BST +# WA5lBrilsEoexXBrOn/1wM3rjG0hIsxvF5/YOK78mVRGY6Y7zYJ+uXt4OTOFBwad +# Pv8MklreQZLPnQPtiwop4rlLUYaPCiD4YUqRNbLp8Sgyo9g0iAcZYznTuc+8Q8ZI +# rgwwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEB +# CwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE +# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD +# VQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAe +# Fw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN -# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp -# Z25pbmcgUENBIDIwMTECEzMAAASFXpnsDlkvzdcAAAAABIUwDQYJYIZIAWUDBAIB -# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO -# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEID8/Z0hz8wCpH2YjVYR3wACO -# qi7toMi0S892RCpCiXnDMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A -# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB -# BQAEggEAR3ofVJe8H1PSnVv5GEV/iNRKzDHBYXTNKjw6gaEwywGiLnvok4fIy+o/ -# pgoyuM4RLT6jq9o/62LWZPnRCXiQiidnt9u6BtjAQFoy9Hyz39SnG3SIfcXwQU6S -# Kn6sdIdkCnp9zgCw0A1um1l9ZESP36cub7lCkog6Qd1N+d5KAMuDMHX4MybWYjva -# YmW+c3RMH4HoBd6igF/hUaz0VTf+yrdIUaBIJ9UlWTMVkwokmQ9I79IwPU5hHnRu -# Ao8D6p++BagDKmVHo4bY/ADy4GDn4nrLA09mwd0YQPDZvb3K3Z2rIABM0UdS4+lG -# c/pZsaRUT7TE8NzWXP+vWQ9bdkhNbaGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC -# F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq -# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl -# AwQCAQUABCAd+KomD6n/vMp0PpchU0Vc9uK1oIZ/s0smWP9W6KAY4QIGaSc7gduW -# GBMyMDI1MTIxMDIyNDQ0NC41NjdaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV -# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE -# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l -# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046REMwMC0w -# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg -# ghHqMIIHIDCCBQigAwIBAgITMwAAAgO7HlwAOGx0ygABAAACAzANBgkqhkiG9w0B -# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE -# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD -# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yNTAxMzAxOTQy -# NDZaFw0yNjA0MjIxOTQyNDZaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz -# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv -# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z -# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046REMwMC0wNUUwLUQ5NDcxJTAjBgNV -# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB -# AQUAA4ICDwAwggIKAoICAQChl0MH5wAnOx8Uh8RtidF0J0yaFDHJYHTpPvRR16X1 -# KxGDYfT8PrcGjCLCiaOu3K1DmUIU4Rc5olndjappNuOgzwUoj43VbbJx5PFTY/a1 -# Z80tpqVP0OoKJlUkfDPSBLFgXWj6VgayRCINtLsUasy0w5gysD7ILPZuiQjace5K -# xASjKf2MVX1qfEzYBbTGNEijSQCKwwyc0eavr4Fo3X/+sCuuAtkTWissU64k8rK6 -# 0jsGRApiESdfuHr0yWAmc7jTOPNeGAx6KCL2ktpnGegLDd1IlE6Bu6BSwAIFHr7z -# OwIlFqyQuCe0SQALCbJhsT9y9iy61RJAXsU0u0TC5YYmTSbEI7g10dYx8Uj+vh9I -# nLoKYC5DpKb311bYVd0bytbzlfTRslRTJgotnfCAIGMLqEqk9/2VRGu9klJi1j9n -# VfqyYHYrMPOBXcrQYW0jmKNjOL47CaEArNzhDBia1wXdJANKqMvJ8pQe2m8/ciby -# DM+1BVZquNAov9N4tJF4ACtjX0jjXNDUMtSZoVFQH+FkWdfPWx1uBIkc97R+xRLu -# PjUypHZ5A3AALSke4TaRBvbvTBYyW2HenOT7nYLKTO4jw5Qq6cw3Z9zTKSPQ6D5l -# yiYpes5RR2MdMvJS4fCcPJFeaVOvuWFSQ/EGtVBShhmLB+5ewzFzdpf1UuJmuOQT -# TwIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFLIpWUB+EeeQ29sWe0VdzxWQGJJ9MB8G -# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG -# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy -# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w -# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy -# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG -# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD -# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQCQEMbesD6TC08R0oYCdSC452AQrGf/O89G -# Q54CtgEsbxzwGDVUcmjXFcnaJSTNedBKVXkBgawRonP1LgxH4bzzVj2eWNmzGIwO -# 1FlhldAPOHAzLBEHRoSZ4pddFtaQxoabU/N1vWyICiN60It85gnF5JD4MMXyd6pS -# 8eADIi6TtjfgKPoumWa0BFQ/aEzjUrfPN1r7crK+qkmLztw/ENS7zemfyx4kGRgw -# Y1WBfFqm/nFlJDPQBicqeU3dOp9hj7WqD0Rc+/4VZ6wQjesIyCkv5uhUNy2LhNDi -# 2leYtAiIFpmjfNk4GngLvC2Tj9IrOMv20Srym5J/Fh7yWAiPeGs3yA3QapjZTtfr -# 7NfzpBIJQ4xT/ic4WGWqhGlRlVBI5u6Ojw3ZxSZCLg3vRC4KYypkh8FdIWoKirji -# dEGlXsNOo+UP/YG5KhebiudTBxGecfJCuuUspIdRhStHAQsjv/dAqWBLlhorq2OC -# aP+wFhE3WPgnnx5pflvlujocPgsN24++ddHrl3O1FFabW8m0UkDHSKCh8QTwTkYO -# wu99iExBVWlbYZRz2qOIBjL/ozEhtCB0auKhfTLLeuNGBUaBz+oZZ+X9UAECoMhk -# ETjb6YfNaI1T7vVAaiuhBoV/JCOQT+RYZrgykyPpzpmwMNFBD1vdW/29q9nkTWoE -# hcEOO0L9NzCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI -# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw -# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x -# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy -# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC -# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV -# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp -# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg -# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF -# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6 -# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp -# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu -# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E -# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0 -# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q -# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ -# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA -# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw -# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG -# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV -# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj -# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK -# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC -# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX -# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v -# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI -# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j -# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG -# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x -# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC -# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449 -# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM -# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS -# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d -# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn -# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs -# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL -# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL -# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN -# MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp -# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw -# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn -# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkRDMDAtMDVFMC1EOTQ3MSUwIwYDVQQD -# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQDN -# rxRX/iz6ss1lBCXG8P1LFxD0e6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD +# aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0 +# YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGm +# TOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/H +# ZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDc +# wUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62A +# W36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1w +# jjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCG +# MFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ +# 1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP +# 8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFz +# ymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHz +# NgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3 +# xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsG +# AQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/ +# LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEG +# DCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29m +# dC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYB +# BQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8G +# A1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQw +# VgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9j +# cmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUF +# BwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br +# aS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQEL +# BQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfC +# cTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AF +# vonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l +# 9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn +# 8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5m +# O0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyx +# TkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4 +# S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9 +# y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM +# +Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhw +# RNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCCAkEC +# AQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv +# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 +# aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0 +# ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoyQTFBLTA1RTAtRDk0NzElMCMG +# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIa +# AxUAOsyf2b6riPKnnXlIgIL2f53PUsKggYMwgYCkfjB8MQswCQYDVQQGEwJVUzET +# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV +# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1T +# dGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO2zHvgwIhgPMjAyNjA1MTYx +# NjUxMDRaGA8yMDI2MDUxNzE2NTEwNFowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA +# 7bMe+AIBADAKAgEAAgIhcwIB/zAHAgEAAgISqjAKAgUA7bRweAIBADA2BgorBgEE +# AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag +# MA0GCSqGSIb3DQEBCwUAA4IBAQBjb6eELCy7c+QKNzPmI4O0riW248wi0uSI0DKx +# /ZRwZgOShcyQUcjn6Q/SGMTQ9cqeJe/IhyZbzUCTATtImhIybXreibtT2cAMO4M6 +# 3gGHvd96iu8Q49zOYR182At7wmlUTeMrsj51LpZkWCUy4ZSeOSri83Aong5MKGMF +# Kt2xnKiSYL2tXIm0zcTpX8xjXMMWFb7wa0CBnHn5ZMYJVtuntxE1B4wJyMZktBS6 +# WkP9XpGCXB1X7VhYM3vvtBFq2vA5neloI7waPPPTU+0hK6IX7+c1U776yDnJW4jc +# OUJsj8rjwrS+Gc/gPt98V3kMoGvxG0eGEzdjkmblUlhT1FwpMYIEDTCCBAkCAQEw +# gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT +# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE +# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIQq83kFhjvObAA +# AQAAAhAwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B +# CRABBDAvBgkqhkiG9w0BCQQxIgQgn7/AUpcQFhG3OcHi0Aeq56NC1uclpcmcCkGu +# +HK5un4wgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDD1SHufsjzY59S1iHU +# QY9hnsKSrJPg5a9Mc4YnGmPHxjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w -# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7OQtPTAiGA8yMDI1MTIxMDE3MzI0 -# NVoYDzIwMjUxMjExMTczMjQ1WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDs5C09 -# AgEAMAcCAQACAgjlMAcCAQACAhNOMAoCBQDs5X69AgEAMDYGCisGAQQBhFkKBAIx -# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI -# hvcNAQELBQADggEBAFXCcBVLkxGEigIad7gAMsj2+SQdBANpzq4qPJXOu81TM3HC -# rAkCUTm3FRNc6YPdpfvl07lGlv/NHFCLyXL20d6PZ/1wlF5+WR2OvWjrktwDxYv8 -# cZqk7BrV9SB8xBe/GwVi7smKmlXhznqA6lFPO+VNfOwWcxn0H2yxEsAJKyDmgx/7 -# M8xnMTKeK8ulgSy4EoyGgFIO+nGHqxS0yaXe+OgzErkaavB1Qw7jfmm5/wlBCnwz -# 0UsbaequeL9UjA6FUw3Cc3F+3/D38BzyjJtTxjUVn+QiVWwOfikRJ2F7oZwpsJo3 -# yNIVpwJFpIV6VsqtxzaF0KQZBpS2lBGxVA17pFcxggQNMIIECQIBATCBkzB8MQsw -# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u -# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy -# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAgO7HlwAOGx0ygABAAACAzAN -# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G -# CSqGSIb3DQEJBDEiBCApggCahSc04fWyIz1KF4aeejwqHefyj2gzz7p9QsluFTCB -# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIEsD3RtxlvaTxFOZZnpQw0DksPmV -# duo5SyK9h9w++hMtMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh -# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD -# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw -# MTACEzMAAAIDux5cADhsdMoAAQAAAgMwIgQgwIRXpw5w7cRbWSfOFZ15Z2Nf90Hi -# Ms/hZSpx+kv4aHcwDQYJKoZIhvcNAQELBQAEggIAM+zwgqPLhBOAumqVnUO/YRh7 -# sePgzUWqveSw/J01TAD8JVXufiGmu4neLrGFki/Nz8ytun2DhJP/3xDRu39y/9Pb -# t2qKabzaoASwH95fTjHLYEp0PhqkEZ1hkaaYjVC3TAG0LgU2mrvkEjL3doD5MXu8 -# WWQGcnB0Wera/3POf4ylyQbUUnzo/Pl9qUbjPVW/JouzzDzijObLcYp7IDgIDxGL -# sVJqMgzP1ZWBWsjjx4J0YiYORUnIVKWKPXt/0O3X9VO3zDfOnWRLF8mJj+ybEnqa -# Wd8LxLJnCxpmTAjtELLgC46UB0N4GHR0+ymSba35Ciz4Kzc+7R9E1Ajy1yd2rmGR -# M2u/eAV8MvKybIzgTd9Lukk9KJ5lvzV52CuYyzHOzYgcNt/mFgvM6gfMAef3CeN0 -# EU7ECvTEYqno7krSRi6+HD+R14+7EwXbiR0E+KAB2Ppgj7GqHWKeL/Owyv0A1oEa -# 4ocdqMApLcY908U7IzNu5qo7PPas/RBsB9J52++fyZ/9RyP31IYKu8/5xI5Ef7aH -# XIopbEpuMHpHeuWlYWlfkULa5tjk4iPVCTRVsgn7IimLY/wgVOLL4ueOzZZ6aNws -# Q37w/ocvXIH/qXUllulfh5vINVYqXK3d+l0QT8LCMIxXpJSSgtcFcPJG6aSdOFRQ -# r6EOj+C9DH5MueMd9SY= +# IFBDQSAyMDEwAhMzAAACEKvN5BYY7zmwAAEAAAIQMCIEIC5b102d224evVIH68x9 +# lEf8Au5vChR0bDNP4SViDwl6MA0GCSqGSIb3DQEBCwUABIICAHMgBiQOozDAIR+S +# O28IjquhCukrxIucbxjLUxu4ltpxavA+cedbwkdNPAMFAYEaLHeEYutciK6N5f4R +# VIJX/OSx2ZL6ac/xB5A/7rdoaHV7PK+F1y807INXMA2HPnJGDVbi7HD7nJp26Fuz +# P2K3aGE3kK06C2jXsjdqWsAZv32k1daX+OqFGJdOsmmua/qlF0AMWTXL7rsBStOH +# 5YI/KXtAsd9vvUbqcCMGeGmnRk6V7NmsKbQIkZ3kDk6YfKoZWmSknCKHF6LzCPkI +# bZSI8PNhcn19VUl9VS4WXGyWeGG5zBkOXnImAIKlaLMxR8Z9/kQVx84TrsDBl6Yh +# 34t6ISVDkQtre8SBG6fdjyWKmCOSntciVUlEom8ZwkadixIaf8nOoUJSYq5fl+4J +# HkcUaY79lJKL5DiENwdC4Lt6eNabRo3SPbQE7IQWbWx5h3et1QDitsZ7Gg9yAHaZ +# LS/t/SIAy+OXLXNxyttcyrbDvFI8A8tFE9P1w+3MC1n2cveqd6Wu07Z4l74Mj0LX +# yR+0h/KjfE6SPaL+N20E4CjM0bxRKZ+CuFj/A3tykEADdzFtsEfzUzTDkD3cJpOj +# fojWQNX1ly8Tu3G/WdAFqzrln2y8jCbVdPIqo1ntbu+BbYom1VQqAihYQB855+pG +# D8bOWbnrvhUua7gDFFDA762iZMxD # SIG # End signature block diff --git a/externals/install-dotnet.sh b/externals/install-dotnet.sh index c442946..bd13ffa 100755 --- a/externals/install-dotnet.sh +++ b/externals/install-dotnet.sh @@ -1003,12 +1003,12 @@ copy_files_or_dirs_from_list() { 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 + if [ "$override" = true ] || (! ([ -d "$target" ] || [ -e "$target" ] || [ -L "$target" ])); then mkdir -p "$out_path/$(dirname "$path")" - if [ -d "$target" ]; then + if [ -d "$target" ] || [ -L "$target" ]; then rm -rf "$target" fi - cp -R $override_switch "$root_path/$path" "$target" + cp -RP $override_switch "$root_path/$path" "$target" fi done } @@ -1053,8 +1053,8 @@ extract_dotnet_package() { 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" + find "$temp_out_path" \( -type f -o -type l \) | 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 -o -type l \) | 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" From 98af08bf649b02f9e94783278906d34780362f6f Mon Sep 17 00:00:00 2001 From: js6pak Date: Wed, 27 May 2026 00:43:27 +0200 Subject: [PATCH 4/5] Support global.json's rollForward latest* variants (#538) * Rename toolcache directory instead of moving to tmp This further enhances time savings seen in #213, down from a few minutes to seconds. * Remove branch constraint on workflows used for testing This means you are going to be notified of CI issues quicker, especially if you have to wait for a confirmation before running the workflows on your PR. * Support global.json's rollForward latest* variants * Remove e2e-test's dependency on Microsoft.NET.Test.Sdk The newer versions are net8.0/net462+ only, making older .NET sdks more annoying to test * Revert "Remove e2e-test's dependency on Microsoft.NET.Test.Sdk" This reverts commit 14839a662649cdea79ccecc3a7ce9a91386185b4. * Don't run unsupported versions in global.json tests * Revert "Remove branch constraint on workflows used for testing" This reverts commit d9dae35030614ced7b174b151463481173bfe2a3. --- .github/workflows/e2e-tests.yml | 75 +++++++++++++++++++++++++++++++++ __tests__/clear-toolcache.ps1 | 4 +- dist/setup/index.js | 20 +++++++-- src/setup-dotnet.ts | 24 +++++++++-- 4 files changed, 115 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 4bda39d..406bf0e 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -257,6 +257,81 @@ jobs: shell: pwsh run: __tests__/verify-dotnet.ps1 -Patterns "^9.0", "^10.0" + test-setup-global-json-rollforward-latestmajor: + runs-on: ${{ matrix.operating-system }} + strategy: + fail-fast: false + matrix: + operating-system: [ubuntu-latest, windows-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Clear toolcache + shell: pwsh + run: __tests__/clear-toolcache.ps1 ${{ runner.os }} + - name: Write global.json + shell: bash + run: | + mkdir subdirectory + echo '{"sdk":{"version": "3.1.0","rollForward": "latestMajor"}}' > ./subdirectory/global.json + - name: Setup dotnet + uses: ./ + with: + global-json-file: ./subdirectory/global.json + - name: Verify dotnet + shell: pwsh + run: __tests__/verify-dotnet.ps1 -Patterns "^(?!3)" + + test-setup-global-json-rollforward-latestfeature: + runs-on: ${{ matrix.operating-system }} + strategy: + fail-fast: false + matrix: + operating-system: [ubuntu-latest, windows-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Clear toolcache + shell: pwsh + run: __tests__/clear-toolcache.ps1 ${{ runner.os }} + - name: Write global.json + shell: bash + run: | + mkdir subdirectory + echo '{"sdk":{"version": "10.0.100","rollForward": "latestFeature"}}' > ./subdirectory/global.json + - name: Setup dotnet + uses: ./ + with: + global-json-file: ./subdirectory/global.json + - name: Verify dotnet + shell: pwsh + run: __tests__/verify-dotnet.ps1 -Patterns "^10.0.(?!1)" + + test-setup-global-json-rollforward-latestpatch: + runs-on: ${{ matrix.operating-system }} + strategy: + fail-fast: false + matrix: + operating-system: [ubuntu-latest, windows-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Clear toolcache + shell: pwsh + run: __tests__/clear-toolcache.ps1 ${{ runner.os }} + - name: Write global.json + shell: bash + run: | + mkdir subdirectory + echo '{"sdk":{"version": "10.0.100","rollForward": "latestPatch"}}' > ./subdirectory/global.json + - name: Setup dotnet + uses: ./ + with: + global-json-file: ./subdirectory/global.json + - name: Verify dotnet + shell: pwsh + run: __tests__/verify-dotnet.ps1 -Patterns "^10.0.1(?!00)" + test-setup-global-json-only: runs-on: ${{ matrix.operating-system }} strategy: diff --git a/__tests__/clear-toolcache.ps1 b/__tests__/clear-toolcache.ps1 index 5589ec1..a8bd902 100644 --- a/__tests__/clear-toolcache.ps1 +++ b/__tests__/clear-toolcache.ps1 @@ -6,8 +6,8 @@ $dotnetPaths = @{ foreach ($srcPath in $dotnetPaths[$args[0]]) { if (Test-Path $srcPath) { - Write-Host "Move $srcPath path" - $dstPath = Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName()) + $dstPath = "$srcPath-" + [IO.Path]::GetRandomFileName() + Write-Host "Moving $srcPath to $dstPath" Move-Item -Path $srcPath -Destination $dstPath } } \ No newline at end of file diff --git a/dist/setup/index.js b/dist/setup/index.js index ac2cc6c..3b3ad2c 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -79244,9 +79244,23 @@ function getVersionFromGlobalJson(globalJsonPath) { if (globalJson.sdk && globalJson.sdk.version) { version = globalJson.sdk.version; const rollForward = globalJson.sdk.rollForward; - if (rollForward && rollForward === 'latestFeature') { - const [major, minor] = version.split('.'); - version = `${major}.${minor}`; + if (rollForward) { + const [major, minor, featurePatch] = version.split('.'); + const feature = featurePatch.substring(0, 1); + switch (rollForward) { + case 'latestMajor': + version = ''; + break; + case 'latestMinor': + version = `${major}`; + break; + case 'latestFeature': + version = `${major}.${minor}`; + break; + case 'latestPatch': + version = `${major}.${minor}.${feature}xx`; + break; + } } } return version; diff --git a/src/setup-dotnet.ts b/src/setup-dotnet.ts index 4950f8f..969adbc 100644 --- a/src/setup-dotnet.ts +++ b/src/setup-dotnet.ts @@ -207,9 +207,27 @@ function getVersionFromGlobalJson(globalJsonPath: string): string { if (globalJson.sdk && globalJson.sdk.version) { version = globalJson.sdk.version; const rollForward = globalJson.sdk.rollForward; - if (rollForward && rollForward === 'latestFeature') { - const [major, minor] = version.split('.'); - version = `${major}.${minor}`; + if (rollForward) { + const [major, minor, featurePatch] = version.split('.'); + const feature = featurePatch.substring(0, 1); + + switch (rollForward) { + case 'latestMajor': + version = ''; + break; + + case 'latestMinor': + version = `${major}`; + break; + + case 'latestFeature': + version = `${major}.${minor}`; + break; + + case 'latestPatch': + version = `${major}.${minor}.${feature}xx`; + break; + } } } return version; From 9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 Mon Sep 17 00:00:00 2001 From: Priya Gupta <147705955+priyagupta108@users.noreply.github.com> Date: Thu, 28 May 2026 08:15:27 +0530 Subject: [PATCH 5/5] Add rollForward note in README, improve proxy health check in e2e tests and bump version to v5.3.0 (#738) * docs: add note about latest* rollForward support in global.json * update health check options for squid proxy in e2e tests --- .github/workflows/e2e-tests.yml | 5 +++++ README.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 406bf0e..dc6f3e8 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -577,6 +577,11 @@ jobs: image: ubuntu/squid:latest ports: - 3128:3128 + options: >- + --health-cmd "bash -c ' **Note**: The action supports `latest*` variants of the [rollForward](https://learn.microsoft.com/en-us/dotnet/core/tools/global-json#rollforward) field in `global.json`. When set to `latestPatch`, `latestFeature`, `latestMinor`, or `latestMajor`, the action installs the appropriate SDK version. + ## Caching NuGet Packages The action has a built-in functionality for caching and restoring dependencies. It uses [toolkit/cache](https://github.com/actions/toolkit/tree/main/packages/cache) under the hood for caching global packages data but requires less configuration settings. The `cache` input is optional, and caching is turned off by default. diff --git a/package-lock.json b/package-lock.json index 2f50c9d..7c22f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "setup-dotnet", - "version": "5.0.1", + "version": "5.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "setup-dotnet", - "version": "5.0.1", + "version": "5.3.0", "license": "MIT", "dependencies": { "@actions/cache": "^5.0.5", diff --git a/package.json b/package.json index a7b0afb..d56a35e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "setup-dotnet", - "version": "5.0.1", + "version": "5.3.0", "private": true, "description": "setup dotnet action", "main": "dist/setup/index.js",