From c7e7147fd3e41056e75a761416c06f5fa781b5fb Mon Sep 17 00:00:00 2001 From: Ivan <98037481+IvanZosimov@users.noreply.github.com> Date: Thu, 29 Sep 2022 17:45:25 +0200 Subject: [PATCH] Add ability to write resolved version of SDK into the output variable (#324) --- .github/workflows/workflow.yml | 54 ++++++++++++++++++++++++++++++++++ README.md | 48 ++++++++++++++++++++++++++++++ __tests__/installer.test.ts | 17 +++++++++-- __tests__/setup-dotnet.test.ts | 36 +++++++++++++++++++++++ action.yml | 3 ++ dist/index.js | 46 +++++++++++++++++++++++++---- src/installer.ts | 45 ++++++++++++++++++++++++---- src/setup-dotnet.ts | 19 +++++++++++- src/utils.ts | 1 + 9 files changed, 254 insertions(+), 15 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index eb9241f7e..bf98a7ce5 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -237,6 +237,60 @@ jobs: $version = & dotnet --version Write-Host "Installed version: $version" if (-not ($version.Contains("preview") -or $version.Contains("rc"))) { throw "Unexpected version" } + + test-dotnet-version-output-during-single-version-installation: + 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.401 + uses: ./ + id: step1 + with: + dotnet-version: "6.0.401" + + - name: Verify value of the dotnet-version output + shell: pwsh + run: | + $version = & dotnet --version + Write-Host "Installed version: $version" + if (-not ($version -eq '${{steps.step1.outputs.dotnet-version}}')) { throw "Unexpected output value" } + + test-dotnet-version-output-during-multiple-version-installation: + 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.401, 5.0.408, 7.0.100-rc.1.22431.12 + uses: ./ + id: step2 + with: + dotnet-version: | + 7.0.100-rc.1.22431.12 + 6.0.401 + 5.0.408 + + - name: Verify value of the dotnet-version output + shell: pwsh + run: | + $version = "7.0.100-rc.1.22431.12" + if (-not ($version -eq '${{steps.step2.outputs.dotnet-version}}')) { throw "Unexpected output value" } test-proxy: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 087097486..c802711b7 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,54 @@ steps: ``` > **Note**: It's the only way to push a package to nuget.org feed for macOS/Linux machines due to API key config store limitations. +# Outputs and environment variables + +## Outputs + +### `dotnet-version` + +Using the **dotnet-version** output it's possible to get the installed by the action .NET SDK version. + +**Single version installation** + +In case of a single version installation, the `dotnet-version` output contains the version that is installed by the action. + +```yaml + - uses: actions/setup-dotnet@v3 + id: cp310 + with: + dotnet-version: 3.1.422 + - run: echo '${{ steps.cp310.outputs.dotnet-version }}' # outputs 3.1.422 +``` + +**Multiple version installation** + +In case of a multiple version installation, the `dotnet-version` output contains the latest version that is installed by the action. + +```yaml + - uses: actions/setup-dotnet@v3 + id: cp310 + with: + dotnet-version: | + 3.1.422 + 5.0.408 + - run: echo '${{ steps.cp310.outputs.dotnet-version }}' # outputs 5.0.408 +``` +**Installation from global.json** + +When the `dotnet-version` input is used along with the `global-json-file` input, the `dotnet-version` output contains the version resolved from the `global.json`. + +```yaml + - uses: actions/setup-dotnet@v3 + id: cp310 + with: + dotnet-version: | + 3.1.422 + 5.0.408 + global-json-file: "./global.json" # contains version 2.2.207 + - run: echo '${{ steps.cp310.outputs.dotnet-version }}' # outputs 2.2.207 +``` + ## Environment variables Some environment variables may be necessary for your particular case or to improve logging. Some examples are listed below, but the full list with complete details can be found here: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 9d94eca21..79a90c34b 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -107,6 +107,15 @@ describe('DotnetCoreInstaller tests', () => { expect(process.env.PATH?.startsWith(toolDir)).toBe(true); }, 600000); //This needs some time to download on "slower" internet connections + it('Returns string with installed SDK version', async () => { + const version = '3.1.120'; + let installedVersion: string; + + installedVersion = await getDotnet(version); + + expect(installedVersion).toBe('3.1.120'); + }, 600000); + it('Throws if no location contains correct dotnet version', async () => { await expect(async () => { await getDotnet('1000.0.0'); @@ -267,11 +276,15 @@ function normalizeFileContents(contents: string): string { .replace(new RegExp('\r', 'g'), '\n'); } -async function getDotnet(version: string, quality: string = ''): Promise { +async function getDotnet( + version: string, + quality: string = '' +): Promise { const dotnetInstaller = new installer.DotnetCoreInstaller( version, quality as QualityOptions ); - await dotnetInstaller.installDotnet(); + 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 4cd84b6c1..1b2e3cf7a 100644 --- a/__tests__/setup-dotnet.test.ts +++ b/__tests__/setup-dotnet.test.ts @@ -1,4 +1,5 @@ import * as io from '@actions/io'; +import * as core from '@actions/core'; import fs from 'fs'; import os from 'os'; import path from 'path'; @@ -20,6 +21,12 @@ if (IS_WINDOWS) { const tempDir = path.join(__dirname, 'runner', 'temp2'); describe('setup-dotnet tests', () => { + let getInputSpy = jest.spyOn(core, 'getInput'); + let getMultilineInputSpy = jest.spyOn(core, 'getMultilineInput'); + let setOutputSpy = jest.spyOn(core, 'setOutput'); + + let inputs = {} as any; + beforeAll(async () => { process.env.RUNNER_TOOL_CACHE = toolDir; process.env.DOTNET_INSTALL_DIR = toolDir; @@ -59,4 +66,33 @@ describe('setup-dotnet tests', () => { expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true); } }, 400000); + + 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']; + + getMultilineInputSpy.mockImplementation(input => inputs[input]); + + await setup.run(); + + expect(setOutputSpy).toBeCalledWith('dotnet-version', '6.0.401'); + }, 400000); + + it("Sets output with the version specified in global.json, if it's present", async () => { + const globalJsonPath = path.join(process.cwd(), 'global.json'); + const jsonContents = `{${os.EOL}"sdk": {${os.EOL}"version": "3.0.103"${os.EOL}}${os.EOL}}`; + if (!fs.existsSync(globalJsonPath)) { + fs.writeFileSync(globalJsonPath, jsonContents); + } + + inputs['dotnet-version'] = ['3.1.201', '6.0.401']; + inputs['global-json-file'] = './global.json'; + + getMultilineInputSpy.mockImplementation(input => inputs[input]); + + getInputSpy.mockImplementation(input => inputs[input]); + + await setup.run(); + + expect(setOutputSpy).toBeCalledWith('dotnet-version', '3.0.103'); + }, 400000); }); diff --git a/action.yml b/action.yml index f259c3bb7..dafec8689 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,9 @@ inputs: description: 'Optional OWNER for using packages from GitHub Package Registry organizations/users other than the current repository''s owner. Only used if a GPR URL is also provided in source-url' config-file: description: 'Optional NuGet.config location, if your NuGet.config isn''t located in the root of the repo.' +outputs: + dotnet-version: + description: 'Contains the installed by action .NET SDK version for reuse.' runs: using: 'node16' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 9e6f7b426..217682101 100644 --- a/dist/index.js +++ b/dist/index.js @@ -189,6 +189,7 @@ 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 semver_1 = __importDefault(__nccwpck_require__(5911)); const utils_1 = __nccwpck_require__(918); @@ -284,8 +285,8 @@ class DotnetCoreInstaller { } else { // This is the default set in install-dotnet.sh - core.addPath(path_1.default.join(process.env['HOME'] + '', '.dotnet')); - core.exportVariable('DOTNET_ROOT', path_1.default.join(process.env['HOME'] + '', '.dotnet')); + core.addPath(DotnetCoreInstaller.installationDirectoryMac); + core.exportVariable('DOTNET_ROOT', DotnetCoreInstaller.installationDirectoryMac); } } } @@ -332,11 +333,11 @@ class DotnetCoreInstaller { if (process.env['no_proxy'] != null) { scriptArguments.push(`-ProxyBypassList ${process.env['no_proxy']}`); } - scriptArguments.push(`-InstallDir '${DotnetCoreInstaller.installationDirectoryWindows}'`); + scriptArguments.push('-InstallDir', `'${DotnetCoreInstaller.installationDirectoryWindows}'`); // process.env must be explicitly passed in for DOTNET_INSTALL_DIR to be used scriptPath = (yield io.which('pwsh', false)) || (yield io.which('powershell', true)); - scriptArguments = [...windowsDefaultOptions, scriptArguments.join(' ')]; + scriptArguments = windowsDefaultOptions.concat(scriptArguments); } else { fs_1.chmodSync(escapedScript, '777'); @@ -351,17 +352,31 @@ class DotnetCoreInstaller { if (utils_1.IS_LINUX) { scriptArguments.push('--install-dir', DotnetCoreInstaller.installationDirectoryLinux); } + if (utils_1.IS_MAC) { + scriptArguments.push('--install-dir', DotnetCoreInstaller.installationDirectoryMac); + } } const { exitCode, stdout } = yield exec.getExecOutput(`"${scriptPath}"`, scriptArguments, { ignoreReturnCode: true }); if (exitCode) { throw new Error(`Failed to install dotnet ${exitCode}. ${stdout}`); } + return this.outputDotnetVersion(dotnetVersion.value, scriptArguments[scriptArguments.length - 1]); + }); + } + outputDotnetVersion(version, installationPath) { + return __awaiter(this, void 0, void 0, function* () { + let versionsOnRunner = yield promises_1.readdir(path_1.default.join(installationPath.replace(/'/g, ''), 'sdk')); + let installedVersion = semver_1.default.maxSatisfying(versionsOnRunner, version, { + includePrerelease: true + }); + return installedVersion; }); } } exports.DotnetCoreInstaller = DotnetCoreInstaller; DotnetCoreInstaller.installationDirectoryWindows = path_1.default.join(process.env['PROGRAMFILES'] + '', 'dotnet'); DotnetCoreInstaller.installationDirectoryLinux = '/usr/share/dotnet'; +DotnetCoreInstaller.installationDirectoryMac = path_1.default.join(process.env['HOME'] + '', '.dotnet'); /***/ }), @@ -408,6 +423,7 @@ const core = __importStar(__nccwpck_require__(2186)); const installer_1 = __nccwpck_require__(1480); const fs = __importStar(__nccwpck_require__(7147)); const path_1 = __importDefault(__nccwpck_require__(1017)); +const semver_1 = __importDefault(__nccwpck_require__(5911)); const auth = __importStar(__nccwpck_require__(8527)); const qualityOptions = [ 'daily', @@ -429,6 +445,7 @@ function run() { // Proxy, auth, (etc) are still set up, even if no version is identified // const versions = core.getMultilineInput('dotnet-version'); + const installedDotnetVersions = []; const globalJsonFileInput = core.getInput('global-json-file'); if (globalJsonFileInput) { const globalJsonPath = path_1.default.join(process.cwd(), globalJsonFileInput); @@ -454,7 +471,8 @@ function run() { const uniqueVersions = new Set(versions); for (const version of uniqueVersions) { dotnetInstaller = new installer_1.DotnetCoreInstaller(version, quality); - yield dotnetInstaller.installDotnet(); + const installedVersion = yield dotnetInstaller.installDotnet(); + installedDotnetVersions.push(installedVersion); } installer_1.DotnetCoreInstaller.addToPath(); } @@ -463,6 +481,13 @@ 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); const matchersPath = path_1.default.join(__dirname, '..', '.github'); core.info(`##[add-matcher]${path_1.default.join(matchersPath, 'csc.json')}`); } @@ -498,9 +523,10 @@ run(); "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.IS_LINUX = exports.IS_WINDOWS = void 0; +exports.IS_MAC = exports.IS_LINUX = exports.IS_WINDOWS = void 0; exports.IS_WINDOWS = process.platform === 'win32'; exports.IS_LINUX = process.platform === 'linux'; +exports.IS_MAC = process.platform === 'darwin'; /***/ }), @@ -25688,6 +25714,14 @@ 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 24bab1b68..041b65579 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -4,9 +4,10 @@ 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 semver from 'semver'; -import {IS_LINUX, IS_WINDOWS} from './utils'; +import {IS_LINUX, IS_WINDOWS, IS_MAC} from './utils'; import {QualityOptions} from './setup-dotnet'; export interface DotnetVersion { @@ -116,6 +117,10 @@ export class DotnetCoreInstaller { 'dotnet' ); private static readonly installationDirectoryLinux = '/usr/share/dotnet'; + private static readonly installationDirectoryMac = path.join( + process.env['HOME'] + '', + '.dotnet' + ); static addToPath() { if (process.env['DOTNET_INSTALL_DIR']) { @@ -136,10 +141,10 @@ export class DotnetCoreInstaller { ); } else { // This is the default set in install-dotnet.sh - core.addPath(path.join(process.env['HOME'] + '', '.dotnet')); + core.addPath(DotnetCoreInstaller.installationDirectoryMac); core.exportVariable( 'DOTNET_ROOT', - path.join(process.env['HOME'] + '', '.dotnet') + DotnetCoreInstaller.installationDirectoryMac ); } } @@ -164,7 +169,7 @@ export class DotnetCoreInstaller { } } - public async installDotnet() { + public async installDotnet(): Promise { const windowsDefaultOptions = [ '-NoLogo', '-Sta', @@ -204,12 +209,13 @@ export class DotnetCoreInstaller { } scriptArguments.push( - `-InstallDir '${DotnetCoreInstaller.installationDirectoryWindows}'` + '-InstallDir', + `'${DotnetCoreInstaller.installationDirectoryWindows}'` ); // process.env must be explicitly passed in for DOTNET_INSTALL_DIR to be used scriptPath = (await io.which('pwsh', false)) || (await io.which('powershell', true)); - scriptArguments = [...windowsDefaultOptions, scriptArguments.join(' ')]; + scriptArguments = windowsDefaultOptions.concat(scriptArguments); } else { chmodSync(escapedScript, '777'); scriptPath = await io.which(escapedScript, true); @@ -229,6 +235,13 @@ export class DotnetCoreInstaller { DotnetCoreInstaller.installationDirectoryLinux ); } + + if (IS_MAC) { + scriptArguments.push( + '--install-dir', + DotnetCoreInstaller.installationDirectoryMac + ); + } } const {exitCode, stdout} = await exec.getExecOutput( `"${scriptPath}"`, @@ -238,5 +251,25 @@ export class DotnetCoreInstaller { if (exitCode) { throw new Error(`Failed to install dotnet ${exitCode}. ${stdout}`); } + + return this.outputDotnetVersion( + dotnetVersion.value, + scriptArguments[scriptArguments.length - 1] + ); + } + + private async outputDotnetVersion( + version, + installationPath + ): Promise { + let versionsOnRunner: string[] = await readdir( + path.join(installationPath.replace(/'/g, ''), 'sdk') + ); + + let installedVersion = semver.maxSatisfying(versionsOnRunner, version, { + includePrerelease: true + })!; + + return installedVersion; } } diff --git a/src/setup-dotnet.ts b/src/setup-dotnet.ts index 23063621a..ba2e419e2 100644 --- a/src/setup-dotnet.ts +++ b/src/setup-dotnet.ts @@ -2,6 +2,7 @@ import * as core from '@actions/core'; import {DotnetCoreInstaller} from './installer'; import * as fs from 'fs'; import path from 'path'; +import semver from 'semver'; import * as auth from './authutil'; const qualityOptions = [ @@ -26,6 +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 globalJsonFileInput = core.getInput('global-json-file'); if (globalJsonFileInput) { @@ -60,7 +62,8 @@ export async function run() { const uniqueVersions = new Set(versions); for (const version of uniqueVersions) { dotnetInstaller = new DotnetCoreInstaller(version, quality); - await dotnetInstaller.installDotnet(); + const installedVersion = await dotnetInstaller.installDotnet(); + installedDotnetVersions.push(installedVersion); } DotnetCoreInstaller.addToPath(); } @@ -71,6 +74,20 @@ 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); + const matchersPath = path.join(__dirname, '..', '.github'); core.info(`##[add-matcher]${path.join(matchersPath, 'csc.json')}`); } catch (error) { diff --git a/src/utils.ts b/src/utils.ts index 77886ce0e..ff20ee303 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,2 +1,3 @@ export const IS_WINDOWS = process.platform === 'win32'; export const IS_LINUX = process.platform === 'linux'; +export const IS_MAC = process.platform === 'darwin';