diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 9963bbc..3933a34 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -24,7 +24,7 @@ jobs: - name: Clear toolcache shell: pwsh run: __tests__/clear-toolcache.ps1 ${{ runner.os }} - - name: Setup dotnet 2.2.402 and 3.1.404 + - name: Setup dotnet 2.2.402, 3.1.404 and 3.0.x uses: ./ with: dotnet-version: | @@ -133,6 +133,27 @@ jobs: shell: pwsh run: __tests__/verify-dotnet.ps1 -Patterns "^2.2", "^3.1" + test-ABCxx-syntax: + runs-on: ${{ matrix.operating-system }} + strategy: + fail-fast: false + matrix: + operating-system: [ubuntu-latest, windows-latest, macOS-latest] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Clear toolcache + shell: pwsh + run: __tests__/clear-toolcache.ps1 ${{ runner.os }} + + - name: Setup dotnet 6.0.4xx + uses: ./ + with: + dotnet-version: '6.0.4xx' + - name: Verify dotnet + shell: pwsh + run: __tests__/verify-dotnet.ps1 -Patterns "^6\.0\.4\d{2}" + test-setup-with-wildcard: runs-on: ${{ matrix.operating-system }} strategy: @@ -183,6 +204,31 @@ jobs: shell: pwsh run: __tests__/verify-dotnet.ps1 -Patterns "^2.2", "^3.1" + test-setup-global-json-only: + runs-on: ${{ matrix.operating-system }} + strategy: + fail-fast: false + matrix: + operating-system: [ubuntu-latest, windows-latest, macOS-latest] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Clear toolcache + shell: pwsh + run: __tests__/clear-toolcache.ps1 ${{ runner.os }} + - name: Write global.json + shell: bash + run: | + mkdir subdirectory + echo '{"sdk":{"version": "2.2.207","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 "^2.2" + test-setup-with-dotnet-quality: runs-on: ${{ matrix.operating-system }} strategy: diff --git a/README.md b/README.md index b275fc3..c2f6d55 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,13 @@ The `dotnet-version` input supports following syntax: - **A.B.C** (e.g 6.0.400, 7.0.100-preview.7.22377.5) - installs exact version of .NET SDK - **A.B** or **A.B.x** (e.g. 3.1, 3.1.x) - installs the latest patch version of .NET SDK on the channel `3.1`, including prerelease versions (preview, rc) - **A** or **A.x** (e.g. 3, 3.x) - installs the latest minor version of the specified major tag, including prerelease versions (preview, rc) +- **A.B.Cxx** (e.g. 6.0.4xx) - available since `.NET 5.0` release. Installs the latest version of the specific SDK release, including prerelease versions (preview, rc). ## 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' and 'A.x' formats where the major version is higher than 5. In other cases, `dotnet-quality` input will be ignored. +> **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. ```yml steps: diff --git a/__tests__/__snapshots__/authutil.test.ts.snap b/__tests__/__snapshots__/authutil.test.ts.snap index d310f14..0ea506b 100644 --- a/__tests__/__snapshots__/authutil.test.ts.snap +++ b/__tests__/__snapshots__/authutil.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`authutil tests Existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -15,7 +15,7 @@ exports[`authutil tests Existing config not in repo root, sets up a partial NuGe " `; -exports[`authutil tests Existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -30,7 +30,7 @@ exports[`authutil tests Existing config w/ Azure Artifacts source and NuGet.org, " `; -exports[`authutil tests Existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -45,7 +45,7 @@ exports[`authutil tests Existing config w/ GPR source and NuGet.org, sets up a p " `; -exports[`authutil tests Existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` " @@ -63,7 +63,7 @@ exports[`authutil tests Existing config w/ no GPR sources, sets up a full NuGet. " `; -exports[`authutil tests Existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` " @@ -81,7 +81,7 @@ exports[`authutil tests Existing config w/ no sources, sets up a full NuGet.conf " `; -exports[`authutil tests Existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -96,7 +96,7 @@ exports[`authutil tests Existing config w/ only Azure Artifacts source, sets up " `; -exports[`authutil tests Existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -111,7 +111,7 @@ exports[`authutil tests Existing config w/ only GPR source, sets up a partial Nu " `; -exports[`authutil tests Existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -130,7 +130,7 @@ exports[`authutil tests Existing config w/ two GPR sources, sets up a partial Nu " `; -exports[`authutil tests No existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR 1`] = ` +exports[`authutil tests no existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR 1`] = ` " @@ -148,7 +148,7 @@ exports[`authutil tests No existing config, sets up a full NuGet.config with URL " `; -exports[`authutil tests No existing config, sets up a full NuGet.config with URL and token for other source 1`] = ` +exports[`authutil tests no existing config, sets up a full NuGet.config with URL and token for other source 1`] = ` " @@ -166,7 +166,7 @@ exports[`authutil tests No existing config, sets up a full NuGet.config with URL " `; -exports[`authutil tests No existing config, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` +exports[`authutil tests no existing config, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` " diff --git a/__tests__/authutil.test.ts b/__tests__/authutil.test.ts index d9a0b7a..3435cdf 100644 --- a/__tests__/authutil.test.ts +++ b/__tests__/authutil.test.ts @@ -91,9 +91,9 @@ describe('authutil tests', () => { process.env['NUGET_AUTH_TOKEN'] = ''; }); - it('No existing config, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { + it('no existing config, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -104,10 +104,10 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('No existing config, auth token environment variable not provided, throws', async () => { + it('no existing config, auth token environment variable not provided, throws', async () => { let thrown = false; try { - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -118,10 +118,10 @@ describe('authutil tests', () => { expect(thrown).toBe(true); }); - it('No existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR', async () => { + it('no existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; process.env['INPUT_OWNER'] = 'otherorg'; - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/otherorg/index.json', '', fakeSourcesDirForTesting @@ -132,7 +132,7 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config (invalid), tries to parse an invalid NuGet.config and throws', async () => { + it('existing config (invalid), tries to parse an invalid NuGet.config and throws', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, @@ -141,7 +141,7 @@ describe('authutil tests', () => { fs.writeFileSync(inputNuGetConfigPath, invalidNuGetConfig); let thrown = false; try { - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -152,14 +152,14 @@ describe('authutil tests', () => { expect(thrown).toBe(true); }); - it('Existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { + it('existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, emptyNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -170,14 +170,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { + it('existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, nugetorgNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -188,14 +188,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, gprNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -206,14 +206,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, gprnugetorgNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -224,14 +224,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, twogprNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com', '', fakeSourcesDirForTesting @@ -242,7 +242,7 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ spaces in key, throws for now', async () => { + it('existing config w/ spaces in key, throws for now', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, @@ -251,7 +251,7 @@ describe('authutil tests', () => { fs.writeFileSync(inputNuGetConfigPath, spaceNuGetConfig); let thrown = false; try { - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -262,7 +262,7 @@ describe('authutil tests', () => { expect(thrown).toBe(true); }); - it('Existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigDirectory: string = path.join( fakeSourcesDirForTesting, @@ -274,7 +274,7 @@ describe('authutil tests', () => { ); fs.mkdirSync(inputNuGetConfigDirectory, {recursive: true}); fs.writeFileSync(inputNuGetConfigPath, gprNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', 'subfolder/nuget.config', fakeSourcesDirForTesting @@ -285,14 +285,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, azureartifactsNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json', '', fakeSourcesDirForTesting @@ -303,14 +303,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, azureartifactsnugetorgNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json', '', fakeSourcesDirForTesting @@ -321,9 +321,9 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('No existing config, sets up a full NuGet.config with URL and token for other source', async () => { + it('no existing config, sets up a full NuGet.config with URL and token for other source', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; - await auth.configAuthentication( + auth.configAuthentication( 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json', '', fakeSourcesDirForTesting diff --git a/__tests__/csc.test.ts b/__tests__/csc.test.ts index 85433b7..8d43b39 100644 --- a/__tests__/csc.test.ts +++ b/__tests__/csc.test.ts @@ -1,21 +1,45 @@ import cscFile from '../.github/csc.json'; describe('csc tests', () => { - it('Valid regular expression', async () => { - const regex = cscFile['problemMatcher'][0]['pattern'][0]['regexp']; + test('regular expression in csc.json is valid', async () => { + const regexPattern = cscFile['problemMatcher'][0]['pattern'][0]['regexp']; + const regexResultsMap = cscFile['problemMatcher'][0]['pattern'][0]; - console.log(regex); - const re = new RegExp(regex); + const regex = new RegExp(regexPattern); - // Ideally we would verify that this const stringsToMatch = [ 'Program.cs(10,79): error CS1002: ; expected [/Users/zacharyeisinger/Documents/repo/setup-dotnet/__tests__/sample-broken-csproj/sample.csproj]', "S:\\Msbuild\\src\\Build\\Evaluation\\ExpressionShredder.cs(33,7): error CS1003: Syntax error, ',' expected [S:\\msbuild\\src\\Build\\Microsoft.Build.csproj > Properties:prop]" ]; + // Expected results are calculated according to the csc matcher located in csc.json file + const expectedResults = [ + { + file: 'Program.cs', + line: '10', + severity: 'error', + code: 'CS1002', + message: '; expected', + fromPath: + '/Users/zacharyeisinger/Documents/repo/setup-dotnet/__tests__/sample-broken-csproj/sample.csproj' + }, + { + file: 'S:\\Msbuild\\src\\Build\\Evaluation\\ExpressionShredder.cs', + line: '33', + severity: 'error', + code: 'CS1003', + message: "Syntax error, ',' expected", + fromPath: + 'S:\\msbuild\\src\\Build\\Microsoft.Build.csproj > Properties:prop' + } + ]; - stringsToMatch.forEach(string => { - const matchStr = string.match(re); - console.log(matchStr); - expect(matchStr).toEqual(expect.anything()); + stringsToMatch.map((string, index) => { + const matchedResultsArray = string.match(regex); + for (const propName in expectedResults[index]) { + const propertyIndex = regexResultsMap[propName]; + const expectedPropValue = expectedResults[index][propName]; + const matchedPropValue = matchedResultsArray![propertyIndex]; + expect(matchedPropValue).toEqual(expectedPropValue); + } }); }, 10000); }); diff --git a/__tests__/installation-scripts.test.ts b/__tests__/installation-scripts.test.ts new file mode 100644 index 0000000..ce71ea7 --- /dev/null +++ b/__tests__/installation-scripts.test.ts @@ -0,0 +1,65 @@ +import path from 'path'; +import fs from 'fs'; +import * as hc from '@actions/http-client'; + +const HTTP_CLIENT_OPTIONS = {allowRetries: true, maxRetries: 10} as const; +const TEST_TIMEOUT = 30000; + +describe('Dotnet installation scripts tests', () => { + it( + 'Uses an up to date bash download script', + async () => { + const httpCallbackClient = new hc.HttpClient( + 'setup-dotnet-test', + [], + HTTP_CLIENT_OPTIONS + ); + const response: hc.HttpClientResponse = await httpCallbackClient.get( + 'https://dot.net/v1/dotnet-install.sh' + ); + expect(response.message.statusCode).toBe(200); + const upToDateContents: string = await response.readBody(); + const currentContents: string = fs + .readFileSync( + path.join(__dirname, '..', 'externals', 'install-dotnet.sh') + ) + .toString(); + expect(normalizeFileContents(currentContents)).toBe( + normalizeFileContents(upToDateContents) + ); + }, + TEST_TIMEOUT + ); + + it( + 'Uses an up to date powershell download script', + async () => { + const httpCallbackClient = new hc.HttpClient( + 'setup-dotnet-test', + [], + HTTP_CLIENT_OPTIONS + ); + const response: hc.HttpClientResponse = await httpCallbackClient.get( + 'https://dot.net/v1/dotnet-install.ps1' + ); + expect(response.message.statusCode).toBe(200); + const upToDateContents: string = await response.readBody(); + const currentContents: string = fs + .readFileSync( + path.join(__dirname, '..', 'externals', 'install-dotnet.ps1') + ) + .toString(); + expect(normalizeFileContents(currentContents)).toBe( + normalizeFileContents(upToDateContents) + ); + }, + TEST_TIMEOUT + ); +}); + +function normalizeFileContents(contents: string): string { + return contents + .trim() + .replace(new RegExp('\r\n', 'g'), '\n') + .replace(new RegExp('\r', 'g'), '\n'); +} diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 5e577c9..c9f815e 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -1,298 +1,443 @@ -import * as io from '@actions/io'; -import * as os from 'os'; -import fs from 'fs'; -import path from 'path'; import each from 'jest-each'; -import * as hc from '@actions/http-client'; +import semver from 'semver'; +import * as exec from '@actions/exec'; +import * as core from '@actions/core'; +import * as io from '@actions/io'; import * as installer from '../src/installer'; -import {QualityOptions} from '../src/setup-dotnet'; import {IS_WINDOWS} from '../src/utils'; -import {IS_LINUX} from '../src/utils'; +import {QualityOptions} from '../src/setup-dotnet'; -let toolDir: string; +describe('installer tests', () => { + const env = process.env; -if (IS_WINDOWS) { - toolDir = path.join(process.env['PROGRAMFILES'] + '', 'dotnet'); -} else if (IS_LINUX) { - toolDir = '/usr/share/dotnet'; -} else { - toolDir = path.join(process.env['HOME'] + '', '.dotnet'); -} -const tempDir = path.join(__dirname, 'runner', 'temp'); + beforeEach(() => { + jest.resetModules(); + process.env = {...env}; + }); -process.env['RUNNER_TOOL_CACHE'] = toolDir; -process.env['RUNNER_TEMP'] = tempDir; + describe('DotnetCoreInstaller tests', () => { + const getExecOutputSpy = jest.spyOn(exec, 'getExecOutput'); + const warningSpy = jest.spyOn(core, 'warning'); + const whichSpy = jest.spyOn(io, 'which'); + const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying'); -describe('DotnetCoreInstaller tests', () => { - beforeAll(async () => { - process.env.RUNNER_TOOL_CACHE = toolDir; - process.env.DOTNET_INSTALL_DIR = toolDir; - process.env.RUNNER_TEMP = tempDir; - process.env.DOTNET_ROOT = ''; - try { - await io.rmRF(`${toolDir}/*`); - await io.rmRF(`${tempDir}/*`); - } catch (err) { - console.log( - `Failed to remove test directories, check the error message:${os.EOL}`, - err.message - ); - } - }, 30000); + describe('installDotnet() tests', () => { + whichSpy.mockImplementation(() => Promise.resolve('PathToShell')); - afterEach(async () => { - try { - await io.rmRF(`${toolDir}/*`); - await io.rmRF(`${tempDir}/*`); - } catch (err) { - console.log( - `Failed to remove test directories, check the error message:${os.EOL}`, - err.message - ); - } - }, 30000); + 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 = '3.1.100'; + const inputQuality = '' as QualityOptions; + const errorMessage = 'fictitious error message!'; + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: 1, + stdout: '', + stderr: errorMessage + }); + }); + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + await expect(dotnetInstaller.installDotnet()).rejects.toThrow( + `Failed to install dotnet, exit code: 1. ${errorMessage}` + ); + }); - it('Aquires multiple versions of dotnet', async () => { - const versions = ['2.2.207', '3.1.120']; + it('should return version of .NET SDK after installation complete', async () => { + const inputVersion = '3.1.100'; + const inputQuality = '' as QualityOptions; + const stdout = `Fictitious dotnet version ${inputVersion} is installed`; + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - for (const version of versions) { - await getDotnet(version); - } - expect(fs.existsSync(path.join(toolDir, 'sdk', '2.2.207'))).toBe(true); - expect(fs.existsSync(path.join(toolDir, 'sdk', '3.1.120'))).toBe(true); + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + const installedVersion = await dotnetInstaller.installDotnet(); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true); - } + expect(installedVersion).toBe(inputVersion); + }); - expect(process.env.DOTNET_ROOT).toBeDefined(); - expect(process.env.PATH).toBeDefined(); - expect(process.env.DOTNET_ROOT).toBe(toolDir); - expect(process.env.PATH?.startsWith(toolDir)).toBe(true); - }, 600000); + it(`should supply 'version' argument to the installation script if supplied version is in A.B.C syntax`, async () => { + const inputVersion = '6.0.300'; + const inputQuality = '' as QualityOptions; + const stdout = `Fictitious dotnet version ${inputVersion} is installed`; - it('Acquires version of dotnet if no matching version is installed', async () => { - await getDotnet('3.1.201'); - expect(fs.existsSync(path.join(toolDir, 'sdk', '3.1.201'))).toBe(true); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true); - } + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - expect(process.env.DOTNET_ROOT).toBeDefined(); - expect(process.env.PATH).toBeDefined(); - expect(process.env.DOTNET_ROOT).toBe(toolDir); - expect(process.env.PATH?.startsWith(toolDir)).toBe(true); - }, 600000); //This needs some time to download on "slower" internet connections + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); - it('Acquires generic version of dotnet if no matching version is installed', async () => { - await getDotnet('3.1'); - const directory = fs - .readdirSync(path.join(toolDir, 'sdk')) - .filter(fn => fn.startsWith('3.1.')); - expect(directory.length > 0).toBe(true); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true); - } + await dotnetInstaller.installDotnet(); - expect(process.env.DOTNET_ROOT).toBeDefined(); - expect(process.env.PATH).toBeDefined(); - expect(process.env.DOTNET_ROOT).toBe(toolDir); - expect(process.env.PATH?.startsWith(toolDir)).toBe(true); - }, 600000); //This needs some time to download on "slower" internet connections + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + const expectedArgument = IS_WINDOWS + ? `-Version ${inputVersion}` + : `--version ${inputVersion}`; - it('Returns string with installed SDK version', async () => { - const version = '3.1.120'; + expect(scriptArguments).toContain(expectedArgument); + }); - const installedVersion = await getDotnet(version); + it(`should warn if the 'quality' input is set and the supplied version is in A.B.C syntax`, async () => { + const inputVersion = '6.0.300'; + const inputQuality = 'ga' as QualityOptions; + const stdout = `Fictitious dotnet version ${inputVersion} is installed`; + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - expect(installedVersion).toBe('3.1.120'); - }, 600000); + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); - it('Throws if no location contains correct dotnet version', async () => { - await expect(async () => { - await getDotnet('1000.0.0'); - }).rejects.toThrow(); - }, 30000); + await dotnetInstaller.installDotnet(); - it('Uses an up to date bash download script', async () => { - const httpCallbackClient = new hc.HttpClient('setup-dotnet-test', [], { - allowRetries: true, - maxRetries: 3 - }); - const response: hc.HttpClientResponse = await httpCallbackClient.get( - 'https://dot.net/v1/dotnet-install.sh' - ); - expect(response.message.statusCode).toBe(200); - const upToDateContents: string = await response.readBody(); - const currentContents: string = fs - .readFileSync( - path.join(__dirname, '..', 'externals', 'install-dotnet.sh') - ) - .toString(); - expect(normalizeFileContents(currentContents)).toBe( - normalizeFileContents(upToDateContents) - ); - }, 30000); + expect(warningSpy).toHaveBeenCalledWith( + `The '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 tag is higher than 5. You specified: ${inputVersion}. 'dotnet-quality' input is ignored.` + ); + }); - it('Uses an up to date powershell download script', async () => { - const httpCallbackClient = new hc.HttpClient('setup-dotnet-test', [], { - allowRetries: true, - maxRetries: 3 - }); - const response: hc.HttpClientResponse = await httpCallbackClient.get( - 'https://dot.net/v1/dotnet-install.ps1' - ); - expect(response.message.statusCode).toBe(200); - const upToDateContents: string = await response.readBody(); - const currentContents: string = fs - .readFileSync( - path.join(__dirname, '..', 'externals', 'install-dotnet.ps1') - ) - .toString(); - expect(normalizeFileContents(currentContents)).toBe( - normalizeFileContents(upToDateContents) - ); - }, 30000); -}); + 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 stdout = `Fictitious dotnet version 3.1.100 is installed`; -describe('DotnetVersionResolver tests', () => { - each([ - '3.1', - '3.x', - '3.1.x', - '3.1.*', - '3.1.X', - '3.1.2', - '3.1.0-preview1' - ]).test( - "if valid version: '%s' is supplied, it should return version object with some value", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version - ); - const versionObject = await dotnetVersionResolver.createDotnetVersion(); + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - expect(!!versionObject.value).toBe(true); - } - ); + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); - each([ - '.', - '..', - ' . ', - '. ', - ' .', - ' . . ', - ' .. ', - ' . ', - '-1.-1', - '-1', - '-1.-1.-1', - '..3', - '1..3', - '1..', - '.2.3', - '.2.x', - '*.', - '1.2.', - '1.2.-abc', - 'a.b', - 'a.b.c', - 'a.b.c-preview', - ' 0 . 1 . 2 ', - 'invalid' - ]).test( - "if invalid version: '%s' is supplied, it should throw", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version + await dotnetInstaller.installDotnet(); + + expect(warningSpy).toHaveBeenCalledWith( + `The '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 tag is higher than 5. You specified: ${inputVersion}. 'dotnet-quality' input is ignored.` + ); + }); + + each(['6', '6.0', '6.0.x', '6.0.*', '6.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 exitCode = 0; + const stdout = `Fictitious dotnet version 6.0.0 is installed`; + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: exitCode, + stdout: `${stdout}`, + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); + + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + + await dotnetInstaller.installDotnet(); + + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + const expectedArgument = IS_WINDOWS + ? `-Quality ${inputQuality}` + : `--quality ${inputQuality}`; + + expect(scriptArguments).toContain(expectedArgument); + } ); - await expect( - async () => await dotnetVersionResolver.createDotnetVersion() - ).rejects.toThrow(); - } - ); + each(['6', '6.0', '6.0.x', '6.0.*', '6.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 exitCode = 0; + const stdout = `Fictitious dotnet version 6.0.0 is installed`; + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: exitCode, + stdout: `${stdout}`, + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - each(['3.1', '3.1.x', '3.1.*', '3.1.X']).test( - "if version: '%s' that can be resolved to 'channel' option is supplied, it should set type to 'channel' in version object", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + + await dotnetInstaller.installDotnet(); + + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + const expectedArgument = IS_WINDOWS + ? `-Channel 6.0` + : `--channel 6.0`; + + expect(scriptArguments).toContain(expectedArgument); + } ); - const versionObject = await dotnetVersionResolver.createDotnetVersion(); - - expect(versionObject.type.toLowerCase().includes('channel')).toBe(true); - } - ); - - each(['6.0', '6.0.x', '6.0.*', '6.0.X']).test( - "if version: '%s' that can be resolved to 'channel' option is supplied and its major tag is >= 6, it should set type to 'channel' and qualityFlag to 'true' in version object", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version - ); - const versionObject = await dotnetVersionResolver.createDotnetVersion(); - - expect(versionObject.type.toLowerCase().includes('channel')).toBe(true); - expect(versionObject.qualityFlag).toBe(true); - } - ); - - each(['3.1.2', '3.1.0-preview1']).test( - "if version: '%s' that can be resolved to 'version' option is supplied, it should set quality flag to 'false' and type to 'version' in version object", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version - ); - const versionObject = await dotnetVersionResolver.createDotnetVersion(); - - expect(versionObject.type.toLowerCase().includes('version')).toBe(true); - expect(versionObject.qualityFlag).toBe(false); - } - ); - - each(['3.1.2', '3.1']).test( - 'it should create proper line arguments for powershell/bash installation scripts', - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version - ); - const versionObject = await dotnetVersionResolver.createDotnetVersion(); - const windowsRegEx = new RegExp(/^-[VC]/); - const nonWindowsRegEx = new RegExp(/^--[vc]/); if (IS_WINDOWS) { - expect(windowsRegEx.test(versionObject.type)).toBe(true); - expect(nonWindowsRegEx.test(versionObject.type)).toBe(false); - } else { - expect(nonWindowsRegEx.test(versionObject.type)).toBe(true); - expect(windowsRegEx.test(versionObject.type)).toBe(false); + 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 = '6.0.100'; + const inputQuality = '' as QualityOptions; + const stdout = `Fictitious dotnet version ${inputVersion} is installed`; + + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); + + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + + await dotnetInstaller.installDotnet(); + + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + + expect(scriptArguments).toContain( + `-ProxyAddress ${process.env['https_proxy']}` + ); + }); + + 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 = '6.0.100'; + const inputQuality = '' as QualityOptions; + const stdout = `Fictitious dotnet version 6.0.0 is installed`; + + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); + + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + + await dotnetInstaller.installDotnet(); + + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + + expect(scriptArguments).toContain( + `-ProxyBypassList ${process.env['no_proxy']}` + ); + }); } - } - ); + }); + + describe('addToPath() tests', () => { + it(`should export DOTNET_ROOT env.var with value from DOTNET_INSTALL_DIR env.var`, async () => { + process.env['DOTNET_INSTALL_DIR'] = 'fictitious/dotnet/install/dir'; + installer.DotnetCoreInstaller.addToPath(); + const dotnet_root = process.env['DOTNET_ROOT']; + expect(dotnet_root).toBe(process.env['DOTNET_INSTALL_DIR']); + }); + + it(`should export value from DOTNET_INSTALL_DIR env.var to the PATH`, async () => { + process.env['DOTNET_INSTALL_DIR'] = 'fictitious/dotnet/install/dir'; + installer.DotnetCoreInstaller.addToPath(); + const path = process.env['PATH']; + expect(path).toContain(process.env['DOTNET_INSTALL_DIR']); + }); + }); + }); + + describe('DotnetVersionResolver tests', () => { + describe('createDotNetVersion() tests', () => { + each([ + '3.1', + '3.x', + '3.1.x', + '3.1.*', + '3.1.X', + '3.1.2', + '3.1.0-preview1', + '6.0.2xx' + ]).test( + 'if valid version is supplied (%s), it should return version object with some value', + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + + expect(!!versionObject.value).toBe(true); + } + ); + + each([ + '.', + '..', + ' . ', + '. ', + ' .', + ' . . ', + ' .. ', + ' . ', + '-1.-1', + '-1', + '-1.-1.-1', + '..3', + '1..3', + '1..', + '.2.3', + '.2.x', + '*.', + '1.2.', + '1.2.-abc', + 'a.b', + 'a.b.c', + 'a.b.c-preview', + ' 0 . 1 . 2 ', + 'invalid' + ]).test( + 'if invalid version is supplied (%s), it should throw', + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + + await expect( + async () => await dotnetVersionResolver.createDotNetVersion() + ).rejects.toThrow(); + } + ); + + each(['3', '3.1', '3.1.x', '3.1.*', '3.1.X', '6.0.2xx']).test( + "if version that can be resolved to 'channel' option is supplied (%s), it should set type to 'channel' in version object", + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + + expect(versionObject.type.toLowerCase().includes('channel')).toBe( + true + ); + } + ); + + each(['6.0', '6.0.x', '6.0.*', '6.0.X', '6.0.2xx']).test( + "if version that can be resolved to 'channel' option is supplied and its major tag is >= 6 (%s), it should set type to 'channel' and qualityFlag to 'true' in version object", + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + + expect(versionObject.type.toLowerCase().includes('channel')).toBe( + true + ); + expect(versionObject.qualityFlag).toBe(true); + } + ); + + each(['3.1.2', '3.1.0-preview1']).test( + "if version that can be resolved to 'version' option is supplied (%s), it should set quality flag to 'false' and type to 'version' in version object", + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + + expect(versionObject.type.toLowerCase().includes('version')).toBe( + true + ); + expect(versionObject.qualityFlag).toBe(false); + } + ); + + each(['3.1.2', '3.1']).test( + 'it should create proper line arguments for powershell/bash installation scripts', + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + const windowsRegEx = new RegExp(/^-(Version|Channel)/); + const nonWindowsRegEx = new RegExp(/^--(version|channel)/); + + if (IS_WINDOWS) { + expect(windowsRegEx.test(versionObject.type)).toBe(true); + expect(nonWindowsRegEx.test(versionObject.type)).toBe(false); + } else { + expect(nonWindowsRegEx.test(versionObject.type)).toBe(true); + expect(windowsRegEx.test(versionObject.type)).toBe(false); + } + } + ); + + it(`should throw if dotnet-version is supplied in A.B.Cxx syntax with major tag lower that 5`, async () => { + const version = '3.0.1xx'; + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + await expect( + async () => await dotnetVersionResolver.createDotNetVersion() + ).rejects.toThrow( + `'dotnet-version' was supplied in invalid format: ${version}! The A.B.Cxx syntax is available since the .NET 5.0 release.` + ); + }); + }); + }); }); - -function normalizeFileContents(contents: string): string { - return contents - .trim() - .replace(new RegExp('\r\n', 'g'), '\n') - .replace(new RegExp('\r', 'g'), '\n'); -} - -async function getDotnet(version: string, quality = ''): Promise { - const dotnetInstaller = new installer.DotnetCoreInstaller( - version, - quality as QualityOptions - ); - const installedVersion = await dotnetInstaller.installDotnet(); - installer.DotnetCoreInstaller.addToPath(); - return installedVersion; -} diff --git a/__tests__/setup-dotnet.test.ts b/__tests__/setup-dotnet.test.ts index 76c1da4..2a32d63 100644 --- a/__tests__/setup-dotnet.test.ts +++ b/__tests__/setup-dotnet.test.ts @@ -1,114 +1,173 @@ -import * as io from '@actions/io'; import * as core from '@actions/core'; import fs from 'fs'; -import os from 'os'; -import path from 'path'; +import semver from 'semver'; +import * as auth from '../src/authutil'; import * as setup from '../src/setup-dotnet'; -import {IS_WINDOWS} from '../src/utils'; -import {IS_LINUX} from '../src/utils'; - -let toolDir: string; - -if (IS_WINDOWS) { - toolDir = path.join(process.env['PROGRAMFILES'] + '', 'dotnet'); -} else if (IS_LINUX) { - toolDir = '/usr/share/dotnet'; -} else { - toolDir = path.join(process.env['HOME'] + '', '.dotnet'); -} - -function createGlobalJsonPath(dotnetVersion: string) { - const globalJsonPath = path.join(process.cwd(), 'global.json'); - const jsonContents = `{${os.EOL}"sdk": {${os.EOL}"version": "${dotnetVersion}"${os.EOL}}${os.EOL}}`; - if (!fs.existsSync(globalJsonPath)) { - fs.writeFileSync(globalJsonPath, jsonContents); - } - return globalJsonPath; -} - -const tempDir = path.join(__dirname, 'runner', 'temp2'); +import {DotnetCoreInstaller} from '../src/installer'; describe('setup-dotnet tests', () => { - const getInputSpy = jest.spyOn(core, 'getInput'); - const getMultilineInputSpy = jest.spyOn(core, 'getMultilineInput'); - const setOutputSpy = jest.spyOn(core, 'setOutput'); - const inputs = {} as any; - beforeAll(async () => { - process.env.RUNNER_TOOL_CACHE = toolDir; - process.env.DOTNET_INSTALL_DIR = toolDir; - process.env.RUNNER_TEMP = tempDir; - try { - await io.rmRF(`${toolDir}/*`); - await io.rmRF(`${tempDir}/*`); - } catch (err) { - console.log(err.message); - console.log('Failed to remove test directories'); - } - }, 30000); + const getInputSpy = jest.spyOn(core, 'getInput'); + const getMultilineInputSpy = jest.spyOn(core, 'getMultilineInput'); + const setFailedSpy = jest.spyOn(core, 'setFailed'); + const warningSpy = jest.spyOn(core, 'warning'); + const debugSpy = jest.spyOn(core, 'debug'); + const infoSpy = jest.spyOn(core, 'info'); + const setOutputSpy = jest.spyOn(core, 'setOutput'); - afterEach(async () => { - try { - await io.rmRF(path.join(process.cwd(), 'global.json')); - await io.rmRF(`${toolDir}/*`); - await io.rmRF(`${tempDir}/*`); - } catch (err) { - console.log(err.message); - console.log('Failed to remove test directories'); - } - }, 30000); + const existsSyncSpy = jest.spyOn(fs, 'existsSync'); - it('Acquires version of dotnet from global.json if no matching version is installed', async () => { - createGlobalJsonPath('3.1.201'); - await setup.run(); + const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying'); - expect(fs.existsSync(path.join(toolDir, 'sdk', '3.1.201'))).toBe(true); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true); - } - }, 400000); + const installDotnetSpy = jest.spyOn( + DotnetCoreInstaller.prototype, + 'installDotnet' + ); + const addToPathSpy = jest.spyOn(DotnetCoreInstaller, 'addToPath'); - it("Sets output with the latest installed by action version if global.json file isn't specified", async () => { - inputs['dotnet-version'] = ['3.1.201', '6.0.401']; + const configAuthenticationSpy = jest.spyOn(auth, 'configAuthentication'); - getMultilineInputSpy.mockImplementation(input => inputs[input]); + describe('run() tests', () => { + beforeEach(() => { + getMultilineInputSpy.mockImplementation(input => inputs[input as string]); + getInputSpy.mockImplementation(input => inputs[input as string]); + }); - await setup.run(); + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); - expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '6.0.401'); - }, 400000); + it('should fail the action if global-json-file input is present, but the file does not exist in the file system', async () => { + inputs['global-json-file'] = 'fictitious.json'; + inputs['dotnet-version'] = []; - it("Sets output with the version specified in global.json, if it's present", async () => { - createGlobalJsonPath('3.0.103'); + const expectedErrorMessage = `The specified global.json file '${inputs['global-json-file']}' does not exist`; - inputs['dotnet-version'] = ['3.1.201', '6.0.401']; - inputs['global-json-file'] = './global.json'; + await setup.run(); + expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage); + }); - getMultilineInputSpy.mockImplementation(input => inputs[input]); + test(`if 'dotnet-version' and 'global-json-file' inputs aren't present, should log into debug output, try to find global.json in the repo root, fail and log message into info output`, async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = []; - getInputSpy.mockImplementation(input => inputs[input]); + maxSatisfyingSpy.mockImplementation(() => null); + setOutputSpy.mockImplementation(() => {}); - await setup.run(); + const expectedDebugMessage = + 'No version found, trying to find version from global.json'; + const expectedInfoMessage = `The global.json wasn't found in the root directory. No .NET version will be installed.`; - expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '3.0.103'); - }, 400000); + await setup.run(); - it('Sets output with the version specified in global.json with absolute path', async () => { - const globalJsonPath = createGlobalJsonPath('3.0.103'); + expect(debugSpy).toHaveBeenCalledWith(expectedDebugMessage); + expect(existsSyncSpy).toHaveBeenCalled(); + expect(infoSpy).toHaveBeenCalledWith(expectedInfoMessage); + }); - inputs['dotnet-version'] = ['3.1.201', '6.0.401']; - inputs['global-json-file'] = globalJsonPath; + it('should fail the action if quality is supplied but its value is not supported', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = ['6.0']; + inputs['dotnet-quality'] = 'fictitiousQuality'; - getMultilineInputSpy.mockImplementation(input => inputs[input]); + const expectedErrorMessage = `Value '${inputs['dotnet-quality']}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`; - getInputSpy.mockImplementation(input => inputs[input]); + await setup.run(); + expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage); + }); - await setup.run(); + it('should call installDotnet() multiple times if dotnet-version multiline input is provided', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = ['6.0', '7.0']; + inputs['dotnet-quality'] = ''; - expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '3.0.103'); - }, 400000); + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + + await setup.run(); + expect(installDotnetSpy).toHaveBeenCalledTimes(2); + }); + + it('should call addToPath() after installation complete', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = ['6.0', '7.0']; + inputs['dotnet-quality'] = ''; + + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + addToPathSpy.mockImplementation(() => {}); + + await setup.run(); + expect(addToPathSpy).toHaveBeenCalledTimes(1); + }); + + it('should call auth.configAuthentication() if source-url input is provided', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = []; + inputs['dotnet-quality'] = ''; + inputs['source-url'] = 'fictitious.source.url'; + + configAuthenticationSpy.mockImplementation(() => {}); + + await setup.run(); + expect(configAuthenticationSpy).toHaveBeenCalledWith( + inputs['source-url'], + undefined + ); + }); + + it('should call auth.configAuthentication() with proper parameters if source-url and config-file inputs are provided', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = []; + inputs['dotnet-quality'] = ''; + inputs['source-url'] = 'fictitious.source.url'; + inputs['config-file'] = 'fictitious.path'; + + configAuthenticationSpy.mockImplementation(() => {}); + setOutputSpy.mockImplementation(() => {}); + + await setup.run(); + expect(configAuthenticationSpy).toHaveBeenCalledWith( + inputs['source-url'], + inputs['config-file'] + ); + }); + + it('should call setOutput() after installation complete successfully', async () => { + inputs['dotnet-version'] = ['6.0.300']; + + installDotnetSpy.mockImplementation(() => + Promise.resolve(`${inputs['dotnet-version']}`) + ); + addToPathSpy.mockImplementation(() => {}); + + await setup.run(); + expect(setOutputSpy).toHaveBeenCalledTimes(1); + }); + + it(`shouldn't call setOutput() if parsing dotnet-installer logs failed`, async () => { + inputs['dotnet-version'] = ['6.0.300']; + const warningMessage = `Failed to output the installed version of .NET. The 'dotnet-version' output will not be set.`; + + installDotnetSpy.mockImplementation(() => Promise.resolve(null)); + addToPathSpy.mockImplementation(() => {}); + + await setup.run(); + expect(warningSpy).toHaveBeenCalledWith(warningMessage); + expect(setOutputSpy).not.toHaveBeenCalled(); + }); + + it(`shouldn't call setOutput() if actions didn't install .NET`, async () => { + inputs['dotnet-version'] = []; + const warningMessage = `The 'dotnet-version' output will not be set.`; + + addToPathSpy.mockImplementation(() => {}); + + await setup.run(); + + expect(infoSpy).toHaveBeenCalledWith(warningMessage); + expect(setOutputSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/action.yml b/action.yml index dafec86..acac654 100644 --- a/action.yml +++ b/action.yml @@ -6,7 +6,7 @@ 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' + 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' dotnet-quality: description: 'Optional quality of the build. The possible values are: daily, signed, validated, preview, ga.' global-json-file: diff --git a/dist/index.js b/dist/index.js index 77d557d..85769f4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -237,11 +237,12 @@ const exec = __importStar(__nccwpck_require__(1514)); const io = __importStar(__nccwpck_require__(7436)); const hc = __importStar(__nccwpck_require__(6255)); const fs_1 = __nccwpck_require__(7147); -const promises_1 = __nccwpck_require__(3292); const path_1 = __importDefault(__nccwpck_require__(1017)); const os_1 = __importDefault(__nccwpck_require__(2037)); const semver_1 = __importDefault(__nccwpck_require__(5911)); const utils_1 = __nccwpck_require__(918); +const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6; +const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5; class DotnetVersionResolver { constructor(version) { this.inputVersion = version.trim(); @@ -249,36 +250,55 @@ class DotnetVersionResolver { } resolveVersionInput() { return __awaiter(this, void 0, void 0, function* () { - if (!semver_1.default.validRange(this.inputVersion)) { - throw new Error(`'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x`); + 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`); } if (semver_1.default.valid(this.inputVersion)) { - this.resolvedArgument.type = 'version'; - this.resolvedArgument.value = this.inputVersion; + this.createVersionArgument(); } else { - const [major, minor] = this.inputVersion.split('.'); - if (this.isNumericTag(major)) { - this.resolvedArgument.type = 'channel'; - if (this.isNumericTag(minor)) { - this.resolvedArgument.value = `${major}.${minor}`; - } - else { - const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { - allowRetries: true, - maxRetries: 3 - }); - this.resolvedArgument.value = yield this.getLatestVersion(httpClient, [major, minor]); - } - } - this.resolvedArgument.qualityFlag = +major >= 6 ? true : false; + yield this.createChannelArgument(); } }); } isNumericTag(versionTag) { return /^\d+$/.test(versionTag); } - createDotnetVersion() { + isLatestPatchSyntax() { + var _a, _b; + const majorTag = (_b = (_a = this.inputVersion.match(/^(?\d+)\.\d+\.\d{1}x{2}$/)) === null || _a === void 0 ? void 0 : _a.groups) === null || _b === void 0 ? void 0 : _b.majorTag; + if (majorTag && + parseInt(majorTag) < LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG) { + throw new Error(`The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! The A.B.Cxx syntax is available since the .NET 5.0 release.`); + } + return majorTag ? true : false; + } + createVersionArgument() { + this.resolvedArgument.type = 'version'; + this.resolvedArgument.value = this.inputVersion; + } + createChannelArgument() { + return __awaiter(this, void 0, void 0, function* () { + this.resolvedArgument.type = 'channel'; + const [major, minor] = this.inputVersion.split('.'); + if (this.isLatestPatchSyntax()) { + this.resolvedArgument.value = this.inputVersion; + } + else if (this.isNumericTag(major) && this.isNumericTag(minor)) { + this.resolvedArgument.value = `${major}.${minor}`; + } + else if (this.isNumericTag(major)) { + this.resolvedArgument.value = yield this.getLatestByMajorTag(major); + } + 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'; + } + this.resolvedArgument.qualityFlag = + parseInt(major) >= QUALITY_INPUT_MINIMAL_MAJOR_TAG ? true : false; + }); + } + createDotNetVersion() { return __awaiter(this, void 0, void 0, function* () { yield this.resolveVersionInput(); if (!this.resolvedArgument.type) { @@ -295,17 +315,21 @@ class DotnetVersionResolver { return this.resolvedArgument; }); } - getLatestVersion(httpClient, versionParts) { + getLatestByMajorTag(majorTag) { return __awaiter(this, void 0, void 0, function* () { + const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { + allowRetries: true, + maxRetries: 3 + }); const response = yield 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] === versionParts[0]; + return sdkParts[0] === majorTag; }); if (!releaseInfo) { - throw new Error(`Could not find info for version ${versionParts.join('.')} at ${DotnetVersionResolver.DotNetCoreIndexUrl}`); + throw new Error(`Could not find info for version with major tag: "${majorTag}" at ${DotnetVersionResolver.DotNetCoreIndexUrl}`); } return releaseInfo['channel-version']; }); @@ -344,7 +368,8 @@ class DotnetInstallScript { if (process.env['no_proxy'] != null) { this.scriptArguments.push(`-ProxyBypassList ${process.env['no_proxy']}`); } - this.scriptPath = (yield io.which('pwsh', false)) || (yield io.which('powershell', true)); + this.scriptPath = + (yield io.which('pwsh', false)) || (yield io.which('powershell', true)); }); } setupScriptBash() { @@ -362,7 +387,7 @@ class DotnetInstallScript { this.useArguments(dotnetVersion.type, dotnetVersion.value); } if (quality && !dotnetVersion.qualityFlag) { - core.warning(`'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A and A.x formats where the major tag is higher than 5. You specified: ${dotnetVersion.value}. 'dotnet-quality' input is ignored.`); + core.warning(`The '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 tag is higher than 5. You specified: ${dotnetVersion.value}. 'dotnet-quality' input is ignored.`); return this; } if (quality) { @@ -413,30 +438,28 @@ class DotnetCoreInstaller { this.version = version; this.quality = quality; } - ; installDotnet() { return __awaiter(this, void 0, void 0, function* () { const versionResolver = new DotnetVersionResolver(this.version); - const dotnetVersion = yield versionResolver.createDotnetVersion(); + const dotnetVersion = yield versionResolver.createDotNetVersion(); const installScript = new DotnetInstallScript() .useArguments(utils_1.IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files') .useVersion(dotnetVersion, this.quality); - const { exitCode, stderr } = yield installScript.execute(); + const { exitCode, stderr, stdout } = yield installScript.execute(); if (exitCode) { throw new Error(`Failed to install dotnet, exit code: ${exitCode}. ${stderr}`); } - return this.outputDotnetVersion(dotnetVersion.value); + return this.parseInstalledVersion(stdout); }); } - outputDotnetVersion(version) { - return __awaiter(this, void 0, void 0, function* () { - const installationPath = process.env['DOTNET_INSTALL_DIR']; - const versionsOnRunner = yield (0, promises_1.readdir)(path_1.default.join(installationPath.replace(/'/g, ''), 'sdk')); - const installedVersion = semver_1.default.maxSatisfying(versionsOnRunner, version, { - includePrerelease: true - }); - return installedVersion; - }); + parseInstalledVersion(stdout) { + const regex = /(?\d+\.\d+\.\d+[a-z0-9._-]*)/gm; + const matchedResult = regex.exec(stdout); + if (!matchedResult) { + core.warning(`Failed to parse installed by the script version of .NET`); + return null; + } + return matchedResult.groups.version; } } exports.DotnetCoreInstaller = DotnetCoreInstaller; @@ -533,13 +556,13 @@ function run() { versions.push(getVersionFromGlobalJson(globalJsonPath)); } else { - core.info(`global.json wasn't found in the root directory. No .NET version will be installed.`); + core.info(`The global.json wasn't found in the root directory. No .NET version will be installed.`); } } if (versions.length) { const quality = core.getInput('dotnet-quality'); if (quality && !qualityOptions.includes(quality)) { - throw new Error(`${quality} is not a supported value for '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, signed, validated, preview, ga.`); } let dotnetInstaller; const uniqueVersions = new Set(versions); @@ -555,13 +578,7 @@ function run() { if (sourceUrl) { auth.configAuthentication(sourceUrl, configFile); } - const comparisonRange = globalJsonFileInput - ? versions[versions.length - 1] - : '*'; - const versionToOutput = semver_1.default.maxSatisfying(installedDotnetVersions, comparisonRange, { - includePrerelease: true - }); - core.setOutput('dotnet-version', versionToOutput); + outputInstalledVersion(installedDotnetVersions, globalJsonFileInput); const matchersPath = path_1.default.join(__dirname, '..', '.github'); core.info(`##[add-matcher]${path_1.default.join(matchersPath, 'csc.json')}`); } @@ -586,6 +603,25 @@ function getVersionFromGlobalJson(globalJsonPath) { } return version; } +function outputInstalledVersion(installedVersions, globalJsonFileInput) { + if (!installedVersions.length) { + core.info(`The 'dotnet-version' output will not be set.`); + return; + } + if (installedVersions.includes(null)) { + core.warning(`Failed to output the installed version of .NET. The 'dotnet-version' output will not be set.`); + return; + } + if (globalJsonFileInput) { + const versionToOutput = installedVersions.at(-1); // .NET SDK version parsed from the global.json file is installed last + core.setOutput('dotnet-version', versionToOutput); + return; + } + const versionToOutput = semver_1.default.maxSatisfying(installedVersions, '*', { + includePrerelease: true + }); + core.setOutput('dotnet-version', versionToOutput); +} run(); @@ -21098,14 +21134,6 @@ module.exports = require("fs"); /***/ }), -/***/ 3292: -/***/ ((module) => { - -"use strict"; -module.exports = require("fs/promises"); - -/***/ }), - /***/ 3685: /***/ ((module) => { diff --git a/src/installer.ts b/src/installer.ts index 5147df9..9f27b4e 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -4,7 +4,6 @@ import * as exec from '@actions/exec'; import * as io from '@actions/io'; import * as hc from '@actions/http-client'; import {chmodSync} from 'fs'; -import {readdir} from 'fs/promises'; import path from 'path'; import os from 'os'; import semver from 'semver'; @@ -17,6 +16,8 @@ export interface DotnetVersion { qualityFlag: boolean; } +const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6; +const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5; export class DotnetVersionResolver { private inputVersion: string; private resolvedArgument: DotnetVersion; @@ -27,33 +28,15 @@ export class DotnetVersionResolver { } private async resolveVersionInput(): Promise { - if (!semver.validRange(this.inputVersion)) { + if (!semver.validRange(this.inputVersion) && !this.isLatestPatchSyntax()) { throw new Error( - `'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x` + `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` ); } if (semver.valid(this.inputVersion)) { - this.resolvedArgument.type = 'version'; - this.resolvedArgument.value = this.inputVersion; + this.createVersionArgument(); } else { - const [major, minor] = this.inputVersion.split('.'); - - if (this.isNumericTag(major)) { - this.resolvedArgument.type = 'channel'; - if (this.isNumericTag(minor)) { - this.resolvedArgument.value = `${major}.${minor}`; - } else { - const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { - allowRetries: true, - maxRetries: 3 - }); - this.resolvedArgument.value = await this.getLatestVersion( - httpClient, - [major, minor] - ); - } - } - this.resolvedArgument.qualityFlag = +major >= 6 ? true : false; + await this.createChannelArgument(); } } @@ -61,11 +44,44 @@ export class DotnetVersionResolver { return /^\d+$/.test(versionTag); } - public async createDotnetVersion(): Promise<{ - type: string; - value: string; - qualityFlag: boolean; - }> { + private isLatestPatchSyntax() { + const majorTag = this.inputVersion.match( + /^(?\d+)\.\d+\.\d{1}x{2}$/ + )?.groups?.majorTag; + if ( + majorTag && + parseInt(majorTag) < LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG + ) { + throw new Error( + `The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! The A.B.Cxx syntax is available since the .NET 5.0 release.` + ); + } + return majorTag ? true : false; + } + + private createVersionArgument() { + this.resolvedArgument.type = 'version'; + this.resolvedArgument.value = this.inputVersion; + } + + private async createChannelArgument() { + this.resolvedArgument.type = 'channel'; + const [major, minor] = this.inputVersion.split('.'); + if (this.isLatestPatchSyntax()) { + this.resolvedArgument.value = this.inputVersion; + } 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); + } 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'; + } + this.resolvedArgument.qualityFlag = + parseInt(major) >= QUALITY_INPUT_MINIMAL_MAJOR_TAG ? true : false; + } + + public async createDotNetVersion(): Promise { await this.resolveVersionInput(); if (!this.resolvedArgument.type) { return this.resolvedArgument; @@ -80,10 +96,11 @@ export class DotnetVersionResolver { return this.resolvedArgument; } - private async getLatestVersion( - httpClient: hc.HttpClient, - versionParts: string[] - ): Promise { + 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 ); @@ -92,14 +109,12 @@ export class DotnetVersionResolver { const releaseInfo = releasesInfo.find(info => { const sdkParts: string[] = info['channel-version'].split('.'); - return sdkParts[0] === versionParts[0]; + return sdkParts[0] === majorTag; }); if (!releaseInfo) { throw new Error( - `Could not find info for version ${versionParts.join('.')} at ${ - DotnetVersionResolver.DotNetCoreIndexUrl - }` + `Could not find info for version with major tag: "${majorTag}" at ${DotnetVersionResolver.DotNetCoreIndexUrl}` ); } @@ -127,6 +142,8 @@ export class DotnetInstallScript { : this.setupScriptBash(); } + + private async setupScriptPowershell() { this.scriptArguments = [ '-NoLogo', @@ -169,7 +186,7 @@ export class DotnetInstallScript { if (quality && !dotnetVersion.qualityFlag) { core.warning( - `'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A and A.x formats where the major tag is higher than 5. You specified: ${dotnetVersion.value}. 'dotnet-quality' input is ignored.` + `The '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 tag is higher than 5. You specified: ${dotnetVersion.value}. 'dotnet-quality' input is ignored.` ); return this; } @@ -239,9 +256,9 @@ export class DotnetCoreInstaller { constructor(private version: string, private quality: QualityOptions) {} - public async installDotnet(): Promise { + public async installDotnet(): Promise { const versionResolver = new DotnetVersionResolver(this.version); - const dotnetVersion = await versionResolver.createDotnetVersion(); + const dotnetVersion = await versionResolver.createDotNetVersion(); const installScript = new DotnetInstallScript() .useArguments( @@ -249,7 +266,7 @@ export class DotnetCoreInstaller { ) .useVersion(dotnetVersion, this.quality); - const {exitCode, stderr} = await installScript.execute(); + const {exitCode, stderr, stdout} = await installScript.execute(); if (exitCode) { throw new Error( @@ -257,19 +274,17 @@ export class DotnetCoreInstaller { ); } - return this.outputDotnetVersion(dotnetVersion.value); + return this.parseInstalledVersion(stdout); } - private async outputDotnetVersion(version): Promise { - const installationPath = process.env['DOTNET_INSTALL_DIR']!; - const versionsOnRunner: string[] = await readdir( - path.join(installationPath.replace(/'/g, ''), 'sdk') - ); + private parseInstalledVersion(stdout: string): string | null { + const regex = /(?\d+\.\d+\.\d+[a-z0-9._-]*)/gm; + const matchedResult = regex.exec(stdout); - const installedVersion = semver.maxSatisfying(versionsOnRunner, version, { - includePrerelease: true - })!; - - return installedVersion; + if (!matchedResult) { + core.warning(`Failed to parse installed by the script version of .NET`); + return null; + } + return matchedResult.groups!.version; } } diff --git a/src/setup-dotnet.ts b/src/setup-dotnet.ts index 31c80a2..58de8ef 100644 --- a/src/setup-dotnet.ts +++ b/src/setup-dotnet.ts @@ -27,7 +27,7 @@ export async function run() { // Proxy, auth, (etc) are still set up, even if no version is identified // const versions = core.getMultilineInput('dotnet-version'); - const installedDotnetVersions: string[] = []; + const installedDotnetVersions: (string | null)[] = []; const globalJsonFileInput = core.getInput('global-json-file'); if (globalJsonFileInput) { @@ -48,7 +48,7 @@ export async function run() { versions.push(getVersionFromGlobalJson(globalJsonPath)); } else { core.info( - `global.json wasn't found in the root directory. No .NET version will be installed.` + `The global.json wasn't found in the root directory. No .NET version will be installed.` ); } } @@ -58,7 +58,7 @@ export async function run() { if (quality && !qualityOptions.includes(quality)) { throw new Error( - `${quality} is not a supported value for '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, signed, validated, preview, ga.` ); } @@ -78,19 +78,7 @@ export async function run() { auth.configAuthentication(sourceUrl, configFile); } - const comparisonRange: string = globalJsonFileInput - ? versions[versions.length - 1]! - : '*'; - - const versionToOutput = semver.maxSatisfying( - installedDotnetVersions, - comparisonRange, - { - includePrerelease: true - } - ); - - core.setOutput('dotnet-version', versionToOutput); + outputInstalledVersion(installedDotnetVersions, globalJsonFileInput); const matchersPath = path.join(__dirname, '..', '.github'); core.info(`##[add-matcher]${path.join(matchersPath, 'csc.json')}`); @@ -116,4 +104,37 @@ function getVersionFromGlobalJson(globalJsonPath: string): string { return version; } +function outputInstalledVersion( + installedVersions: (string | null)[], + globalJsonFileInput: string +): void { + if (!installedVersions.length) { + core.info(`The 'dotnet-version' output will not be set.`); + return; + } + + if (installedVersions.includes(null)) { + core.warning( + `Failed to output the installed version of .NET. The 'dotnet-version' output will not be set.` + ); + return; + } + + if (globalJsonFileInput) { + const versionToOutput = installedVersions.at(-1); // .NET SDK version parsed from the global.json file is installed last + core.setOutput('dotnet-version', versionToOutput); + return; + } + + const versionToOutput = semver.maxSatisfying( + installedVersions as string[], + '*', + { + includePrerelease: true + } + ); + + core.setOutput('dotnet-version', versionToOutput); +} + run();