diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3ec658b..3933a34 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -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__/installer.test.ts b/__tests__/installer.test.ts index b26d915..c9f815e 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -48,8 +48,13 @@ describe('installer tests', () => { 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: '', stderr: ''}); + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); }); maxSatisfyingSpy.mockImplementation(() => inputVersion); @@ -65,9 +70,14 @@ describe('installer tests', () => { 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`; getExecOutputSpy.mockImplementation(() => { - return Promise.resolve({exitCode: 0, stdout: '', stderr: ''}); + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); }); maxSatisfyingSpy.mockImplementation(() => inputVersion); @@ -91,9 +101,13 @@ 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 = '6.0.300'; const inputQuality = 'ga' as QualityOptions; - + const stdout = `Fictitious dotnet version ${inputVersion} is installed`; getExecOutputSpy.mockImplementation(() => { - return Promise.resolve({exitCode: 0, stdout: '', stderr: ''}); + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); }); maxSatisfyingSpy.mockImplementation(() => inputVersion); @@ -105,16 +119,21 @@ describe('installer tests', () => { await dotnetInstaller.installDotnet(); expect(warningSpy).toHaveBeenCalledWith( - `'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: ${inputVersion}. '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: ${inputVersion}. 'dotnet-quality' input is ignored.` ); }); 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`; getExecOutputSpy.mockImplementation(() => { - return Promise.resolve({exitCode: 0, stdout: '', stderr: ''}); + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); }); maxSatisfyingSpy.mockImplementation(() => inputVersion); @@ -126,7 +145,7 @@ describe('installer tests', () => { await dotnetInstaller.installDotnet(); expect(warningSpy).toHaveBeenCalledWith( - `'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: ${inputVersion}. '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: ${inputVersion}. 'dotnet-quality' input is ignored.` ); }); @@ -135,10 +154,11 @@ describe('installer tests', () => { 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: `${stdout}`, stderr: '' }); }); @@ -167,10 +187,11 @@ describe('installer tests', () => { 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: `${stdout}`, stderr: '' }); }); @@ -199,9 +220,14 @@ describe('installer tests', () => { 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: '', stderr: ''}); + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); }); maxSatisfyingSpy.mockImplementation(() => inputVersion); @@ -225,9 +251,14 @@ describe('installer tests', () => { 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: '', stderr: ''}); + return Promise.resolve({ + exitCode: 0, + stdout: `${stdout}`, + stderr: '' + }); }); maxSatisfyingSpy.mockImplementation(() => inputVersion); @@ -275,7 +306,8 @@ describe('installer tests', () => { '3.1.*', '3.1.X', '3.1.2', - '3.1.0-preview1' + '3.1.0-preview1', + '6.0.2xx' ]).test( 'if valid version is supplied (%s), it should return version object with some value', async version => { @@ -327,7 +359,7 @@ describe('installer tests', () => { } ); - each(['3', '3.1', '3.1.x', '3.1.*', '3.1.X']).test( + 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( @@ -342,7 +374,7 @@ describe('installer tests', () => { } ); - each(['6.0', '6.0.x', '6.0.*', '6.0.X']).test( + 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( @@ -394,6 +426,18 @@ describe('installer tests', () => { } } ); + + 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.` + ); + }); }); }); }); diff --git a/__tests__/setup-dotnet.test.ts b/__tests__/setup-dotnet.test.ts index 831408c..2a32d63 100644 --- a/__tests__/setup-dotnet.test.ts +++ b/__tests__/setup-dotnet.test.ts @@ -12,6 +12,7 @@ describe('setup-dotnet tests', () => { 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'); @@ -58,7 +59,7 @@ describe('setup-dotnet tests', () => { const expectedDebugMessage = 'No version found, trying to find version from global.json'; - const expectedInfoMessage = `global.json wasn't found in the root directory. No .NET version will be installed.`; + const expectedInfoMessage = `The global.json wasn't found in the root directory. No .NET version will be installed.`; await setup.run(); @@ -72,7 +73,7 @@ describe('setup-dotnet tests', () => { inputs['dotnet-version'] = ['6.0']; inputs['dotnet-quality'] = 'fictitiousQuality'; - const expectedErrorMessage = `${inputs['dotnet-quality']} is not a supported value for '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, signed, validated, preview, ga.`; await setup.run(); expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage); @@ -133,14 +134,40 @@ describe('setup-dotnet tests', () => { ); }); - it('should call setOutput() after installation complete', async () => { + it('should call setOutput() after installation complete successfully', async () => { inputs['dotnet-version'] = ['6.0.300']; - installDotnetSpy.mockImplementation(() => Promise.resolve('')); + 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 8e20d7f..088b68f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -238,11 +238,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(); @@ -250,35 +251,54 @@ 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); } + isLatestPatchSyntax() { + var _b, _c; + const majorTag = (_c = (_b = this.inputVersion.match(/^(?\d+)\.\d+\.\d{1}x{2}$/)) === null || _b === void 0 ? void 0 : _b.groups) === null || _c === void 0 ? void 0 : _c.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(); @@ -296,17 +316,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']; }); @@ -341,7 +365,7 @@ class DotnetCoreInstaller { scriptArguments.push(option, this.quality); } else { - 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: ${this.version}. '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: ${this.version}. 'dotnet-quality' input is ignored.`); } } installDotnet() { @@ -398,22 +422,21 @@ class DotnetCoreInstaller { ignoreReturnCode: true, env: process.env }; - const { exitCode, stderr } = yield exec.getExecOutput(`"${scriptPath}"`, scriptArguments, getExecOutputOptions); + const { exitCode, stdout, stderr } = yield exec.getExecOutput(`"${scriptPath}"`, scriptArguments, getExecOutputOptions); 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; @@ -527,13 +550,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); @@ -549,13 +572,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')}`); } @@ -580,6 +597,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(); @@ -21084,14 +21120,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 ef9e993..a993be8 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}` ); } @@ -171,12 +186,12 @@ export class DotnetCoreInstaller { scriptArguments.push(option, this.quality); } else { 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: ${this.version}. '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: ${this.version}. 'dotnet-quality' input is ignored.` ); } } - public async installDotnet(): Promise { + public async installDotnet(): Promise { const windowsDefaultOptions = [ '-NoLogo', '-Sta', @@ -236,7 +251,7 @@ export class DotnetCoreInstaller { ignoreReturnCode: true, env: process.env as {string: string} }; - const {exitCode, stderr} = await exec.getExecOutput( + const {exitCode, stdout, stderr} = await exec.getExecOutput( `"${scriptPath}"`, scriptArguments, getExecOutputOptions @@ -247,19 +262,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();